Skip to content

refactor(llc)!: route message reminders through the generated client - #3053

Draft
VelikovPetar wants to merge 35 commits into
refactor/flu-953_user_updates_openapi_migrationfrom
refactor/flu-955_reminders_openapi_migration
Draft

VelikovPetar wants to merge 35 commits into
refactor/flu-953_user_updates_openapi_migrationfrom
refactor/flu-955_reminders_openapi_migration

Conversation

@VelikovPetar

Copy link
Copy Markdown
Contributor

Submit a pull request

Linear: FLU-955
Github Issue: -

CLA

  • I have signed the Stream CLA (required).
  • The code changes follow best practices
  • Code changes are tested (add some information if not applicable)

Description of the pull request

Moves message reminders onto the OpenAPI-generated client, as plan group 06 (Reminders). It also adds the mapper from the generated MessageResponse to today's Message; reminders are its first user. Stacked on #3052.

Note

The branch carries more than this change. Besides the final commit, it contains a merge of #3037 (polls, still open against v11) and five not-yet-reviewed commits that make leaf models plain: Moderation (e1ec831), ReactionGroup (ae62a1b), Action (d4e87e1), Reaction (b072b06) and Location (03c00a8). The message mapper depends on both. This PR lands after #3037; the leaf-model commits can be split into their own PR if preferred. Review this change through the last commit, d2c4277.

Reminders (group 06): fully migrated

  • StreamChatClient and Channel: createReminder, updateReminder, deleteReminder and queryReminders return a Result instead of throwing.
  • queryReminders takes limit (default 10), next and prev instead of PaginationParams, and QueryRemindersResponse gains prev.
  • deleteReminder returns a new DeleteReminderResponse.
  • CreateReminderResponse, UpdateReminderResponse and QueryRemindersResponse become immutable freezed envelopes with a non-null duration. Their shared base class, MessageReminderResponse, is removed.
  • MessageReminder becomes a plain freezed model. It loses fromJson, toJson and Equatable; equality is unchanged, and the hand-written copyWith and merge are kept.
  • v1 JSON (Message.reminder, Event.reminder) decodes through the internal MessageReminderV1JsonConverter, which writes the keys v10 did.
  • RemindersRepository over DefaultApi; reminders_api.dart and StreamChatApi.reminders are removed.
  • StreamMessageReminderListController is adapted; its API and behaviour are unchanged.
Generated Public
DefaultApi.createReminder (POST /api/v2/chat/messages/{id}/reminders) StreamChatClient.createReminder, Channel.createReminder
DefaultApi.updateReminder (PATCH /api/v2/chat/messages/{id}/reminders) StreamChatClient.updateReminder, Channel.updateReminder
DefaultApi.deleteReminder (DELETE /api/v2/chat/messages/{id}/reminders) StreamChatClient.deleteReminder, Channel.deleteReminder
DefaultApi.queryReminders (POST /api/v2/chat/reminders/query) StreamChatClient.queryReminders
CreateReminderResponse, UpdateReminderResponse, QueryRemindersResponse same names (ours, freezed)
DeleteReminderResponse DeleteReminderResponse (new, ours)
ReminderResponseData MessageReminder

Message mapping: added, used only by reminders for now

The generated message and everything nested in it map onto today's json_serializable models. Each mapper sits in its own file under repository/mapper/:

File Maps
message_mapper.dart MessageResponse → Message
attachment_mapper.dart Attachment → Attachment, Action → Action
reaction_mapper.dart ReactionResponse → Reaction, ReactionGroupResponse → ReactionGroup
location_mapper.dart SharedLocationResponseData → Location
drafts_mapper.dart DraftResponse → Draft, DraftPayloadResponse → DraftMessage
moderation_mapper.dart (existing) ModerationV2Response → Moderation
reminders_mapper.dart ReminderResponseData → MessageReminder, plus the four envelopes
  • Message gains html, mml, imageLabels and deletedReplyCount as constructor arguments stored in extraData and read back through typed getters. This is the partial promotion User and Member use.
  • core/util/message_decoding.dart (internal) is shared by Message.fromJson and the mapper:
    • withDerivedState(): the type/state derivation, moved verbatim from fromJson;
    • reactionGroupsFromCounts(): reaction groups built from counts and scores, rewritten to work on typed maps with the same results.

Decisions

  • Breaks beyond the README list, approved: PaginationParams → limit/next/prev (the backend reads only those, so the other fields never worked), and removing MessageReminderResponse.
  • deleteReminder returns an envelope, not Result<void>: the spec answers a named DeleteReminderResponse.
  • Mappers never throw: no casts, no !, no throwing constructors on response data. Repositories keep result.map(...) and there is no catching helper.
  • Custom data cannot impersonate a field: the message, attachment, reaction and draft mappers drop custom keys named like the model's own fields, including the ones kept in extraData. This matches v1, where the server drops a colliding custom key. The entries for cid, html and deleted_reply_count are redundant, because those are always written after the custom data, but they are kept for consistency until the full promotion.
  • cid stays raw in extraData, as in v10.
  • mentioned_channel_members is dropped by the v2 mapper: the SDK has no feature built on it. Message.fromJson (WebSocket events, offline cache) still puts it in extraData.
  • DraftMessage keeps html and mml as plain extraData entries, without getters.
  • v1 reminder and reaction converters stay until the WebSocket moves to v2: Event keeps decoding v1 JSON after group 10, so they are labelled "WebSocket v2 (no group)", like the poll converters.
  • A null remindAt on updateReminder clears the due date, as in v10: the backend treats the generated request's explicit null like v10's omitted key.

Temporary until a later step

What Today Removed / completed by
Messages themselves (send, get, query, update) v1, through Message.fromJson Group 10
Message mappers' target Today's json_serializable Message, Attachment, Draft, DraftMessage, Location Group 10 re-points them at the plain models
MessageReminderV1JsonConverter, ReactionV1JsonConverter Event/Message still decode v1 JSON WebSocket events and messages decode v2
html/mml/imageLabels/deletedReplyCount stored in extraData Partial promotion, so extraData keeps what v10 put in it Full promotion to real fields
Custom-key filters in the mappers Keep the extraData-backed values safe from custom data Full promotion
cid raw in extraData; mentioned_channel_members on the v1 path only v10 parity Group 10
Users nested in messages and reminders Mapped through user_mapper.dart, unchanged; nested users carry blocked_user_ids: [] where v10 had none Group 09

Follow-ups

  • Group 09: User's custom-key filter lacks name and image, so a custom key with those names can stand in for a missing real value. It is left as is, consistent with the message filters, until the full promotion.
  • Group 09: blocked_user_ids: [] on nested users.
  • Group 10: re-point the message mappers at the plain models.
  • Pre-existing: Channel's reminder methods throw StateError synchronously on an uninitialized channel, as in v10.

Backend checks

  • Reminders from queryReminders carry their channel, message and user. Create and update carry the message and user. Event payloads carry them only on some paths. Test stubs follow this.
  • A soft delete sets type: "deleted". v2 moderation carries the current action names (remove, bounce, flag, shadow).
  • reaction_counts and reaction_scores always share keys with the groups and are integers; reaction_groups is absent on messages without reactions and on some legacy ones.
  • v2 custom can contain keys named like built-in fields; v1 drops them.

Testing

  • melos run analyze and dart format are clean. Suites pass: stream_chat (1,997), stream_chat_flutter_core (384), stream_chat_persistence (314). generate_plan.py --check reports 0 problems.

  • New client tests (client_{create,update,delete}_reminder_test.dart, client_query_reminders_test.dart):

    • each sends the exact generated request;
    • each stubs a fully populated response (shared message_fixtures.dart, with distinct values across three messages) and compares the whole envelope;
    • each has a failure test.
  • Extended tests: message_test (type/state derivation, reaction-group precedence, the new constructor arguments and getters), v1_json_converters_test (reminder decode, epoch dates, toJson keys, bookmark round trip), message_reminder_test (merge keeps the channel, message and user the update lacks), channel_test (with and without a due date), and the controller test (page order and cursors).

  • Coverage and mutation testing: line coverage is 100% for every new or changed file. Mutation testing (317 mutants) left only survivors with a recorded reason. Three adversarial reviews (v10 parity, tests, API/docs/plan) and two further test passes were applied.

  • Live v1 vs v2 parity on the demo app (throwaway channel): 34 of 34 domain-model comparisons equal, covering create for 16 message types and query for the 15 still listed after a soft delete:

    • a rich message (custom attachment, quote, all mention kinds, reactions, pin, edit, i18n, thread, draft);
    • thread replies, shown in the channel and thread-only;
    • a poll with votes, an answer and a suggested option;
    • static and live location;
    • image, file, video, voice recording and URL preview attachments;
    • silent, from another user, restricted visibility, soft-deleted and deleted for me.

    Also equal: update (set and clear the due date), cursors, and the second page. The only difference is the deferred blocked_user_ids.

Screenshots / Videos

Not applicable: no visible UI change.

🤖 Generated with Claude Code

VelikovPetar and others added 28 commits September 29, 2026 17:31
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	packages/stream_chat/lib/src/client/client.dart
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ient_test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mappers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-openapi-migration

# Conflicts:
#	migrations/v11-migration.md
#	packages/stream_chat/CHANGELOG.md
…igration' into claude/polls-openapi-migration-0dc838

# Conflicts:
#	migrations/v11-migration.md
#	openapi-migration/12-uploads-cdn.md
#	packages/stream_chat/CHANGELOG.md
#	packages/stream_chat/lib/src/core/api/responses.g.dart
…esponse

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…igration' into claude/polls-openapi-migration-0dc838

# Conflicts:
#	packages/stream_chat/CHANGELOG.md
…igration-0dc838

# Conflicts:
#	migrations/v11-migration.md
#	openapi-migration/04-roles-guest-and-app.md
#	openapi-migration/09-users.md
#	openapi-migration/README.md
#	openapi-migration/tool/generate_plan.py
#	packages/stream_chat/CHANGELOG.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	packages/stream_chat/lib/src/ws/events/event.g.dart
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…penapi_migration' into claude/message-mappers-openapi-591b98

# Conflicts:
#	migrations/v11-migration.md
#	openapi-migration/10-messages.md
#	openapi-migration/tool/generate_plan.py
#	packages/stream_chat/CHANGELOG.md
…igration' into claude/message-mappers-openapi-591b98

# Conflicts:
#	openapi-migration/README.md
#	openapi-migration/tool/generate_plan.py
#	packages/stream_chat/lib/src/client/client.dart
#	packages/stream_chat/lib/src/core/api/responses.dart
#	packages/stream_chat/lib/src/core/api/responses.g.dart
#	packages/stream_chat/lib/src/core/models/converters/v1_json_converters.dart
#	packages/stream_chat/lib/src/core/models/message.g.dart
#	packages/stream_chat/lib/src/ws/events/event.g.dart
#	packages/stream_chat/test/src/client/client_test.dart
…enapi_migration' into claude/message-mappers-openapi-591b98

# Conflicts:
#	migrations/v11-migration.md
#	openapi-migration/README.md
#	openapi-migration/tool/generate_plan.py
#	packages/stream_chat/CHANGELOG.md
#	packages/stream_chat/lib/src/core/api/responses.g.dart
#	packages/stream_chat/test/src/client/client_test.dart
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VelikovPetar and others added 6 commits October 9, 2026 14:26
# Conflicts:
#	packages/stream_chat_flutter_core/CHANGELOG.md
…efactor/flu-955_reminders_openapi_migration

# Conflicts:
#	packages/stream_chat_flutter_core/CHANGELOG.md
…oll requests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e custom data revisit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant