Skip to content

refactor(llc, ui)!: route user updates through the generated client - #3052

Open
VelikovPetar wants to merge 23 commits into
refactor/flu-937_user_blocking_openapi_migrationfrom
refactor/flu-953_user_updates_openapi_migration
Open

VelikovPetar wants to merge 23 commits into
refactor/flu-937_user_blocking_openapi_migrationfrom
refactor/flu-953_user_updates_openapi_migration

Conversation

@VelikovPetar

@VelikovPetar VelikovPetar commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Submit a pull request

Linear: FLU-953
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 creating, updating and partially updating users onto the OpenAPI-generated client (plan group 20, User Updates, split out of group 09). As the first group to map a user, it also renames User's and OwnUser's extraData to custom and turns the values that lived in it into fields (README rule 9).

User updates

  • StreamChatClient.updateUser and updateUsers return Result<UpdateUsersResponse> instead of throwing.
  • partialUpdateUser and partialUpdateUsers are renamed updateUserPartial and updateUsersPartial, and return Result<UpdateUsersResponse>; PartialUpdateUserRequest is renamed UpdateUserPartialRequest (freezed, no toJson).
  • UpdateUsersResponse moves to models/response/ as a freezed class with a non-null duration.
  • updateUser sends only the user's name, image, language, visibility, custom data and, for an own user, privacy settings.
  • UsersRepository gains the two operations; users_mapper.dart gains FullUserResponseMapper, UpdateUsersResponseMapper and UpdateUserPartialRequestMapper. The two methods are removed from StreamChatApi.user.

User custom data and fields

  • User.extraData → User.custom (also the constructor and copyWith arguments). custom is the API's custom data, unfiltered, in both directions; toJson leaves out an entry named like one of the user's fields.

  • User.name and image are nullable fields, stored as given. User.nameOrId gives the name, or the id when the name is missing or empty, which is what v10's name returned.

  • New and retyped fields:

    Model Field Type
    User name, image String? fields (were a non-null name and a nullable image getter)
    User nameOrId String getter
    User deactivatedAt, deletedAt DateTime?
    User shadowBanned bool?
    User blockedUserIds List<String> (moved up from OwnUser)
  • OwnUser.fromUser copies the user's fields instead of round-tripping through JSON, where a custom id replaced the user's id, so connectUser connected as the custom id.

  • OwnUser.merge builds through the constructor: a nullable value the incoming user leaves out keeps the current one, except deactivatedAt, deletedAt and shadowBanned, which are cleared.

  • user.updated: for the current user, the handler rebuilds the event's user as an OwnUser through JSON, so the privacy settings their copy carries reach currentUser, as in v10. Other users stay plain Users.

  • ConnectUserDetails.toJson leaves out a custom entry named like a user field, now that custom can hold one.

  • Persistence: users get name, image, deactivated_at, deleted_at, shadow_banned, blocked_user_ids columns and extra_data becomes custom; schema 1100 + 7, so the cache is rebuilt once.

  • UI: every place that names a user by text uses nameOrId (mentions, labels, previews, a11y, translations in all locales). StreamUserAvatar shows its placeholder icon for a user with no name instead of initials taken from the id, and treats an empty image as none, as v10's image did. Filters and sorts read the raw name.

Generated Public
DefaultApi.updateUsers (POST /api/v2/users) StreamChatClient.updateUser, updateUsers
DefaultApi.updateUsersPartial (PATCH /api/v2/users) StreamChatClient.updateUserPartial, updateUsersPartial
UpdateUsersResponse UpdateUsersResponse (ours, freezed)
FullUserResponse, UserResponse User
UpdateUserPartialRequest UpdateUserPartialRequest (ours, freezed; was PartialUpdateUserRequest)
UserRequest built from User by toRequest()

Why

The migration moves every endpoint onto the generated client, which returns a Result for every call. The API calls the custom data custom, and it types the user's name, image and the other values as fields; keeping them in extraData let a custom entry with the same name overwrite them, and a name that fell back to the id could not tell an unnamed user apart.

Notes for reviewers

Important

FullUserResponse maps to User; there is no public full-user type. The generated response answers a FullUserResponse per user: the profile plus the fields only that user can see (devices, mutes, privacy settings, unread counts, …). UpdateUsersResponse keeps v10's Map<String, User> and drops that private part. A client-side token can only update its own user, whose private fields are on currentUser. Mapping the caller's own entry to an OwnUser stays an additive option once Mute, ChannelMute and PrivacySettings have response mappers.

Important

User.name changes meaning; nameOrId is the old getter.

v10 / base This PR
User.name String getter over extraData['name']; the id when the name is missing or '' String? field; null when the user has no name
User.nameOrId — the name, or the id when it is missing or '': the old name
User.image getter over extraData['image']; null when missing or '' String? field
Setting the name name: argument, or extraData: {'name': …} name: argument only; custom: {'name': …} is just custom data

Everything that used the old name to name a user by text now calls nameOrId, so the UI reads the same. Code that relied on name never being empty (string interpolation, .isNotEmpty checks) compiles unchanged and prints null for a user with no name.

Important

The user.updated handler reads the current user's privacy settings back through JSON. User doesn't model privacy_settings, so on the current user's copy of the event they land in custom. Inside the existing current-user check, the handler rebuilds the user with OwnUser.fromJson(user.toJson()), which reads them into privacySettings (verified live, including after a privacy-only change). The round trip is safe because User.toJson leaves out custom keys named like a field. It is a temporary adapter, tagged TODO(openapi-migration) and listed in the README: when the WebSocket moves to v2, the generated UserUpdatedEvent.user (UserResponsePrivacyFields) is mapped once and the handler applies only the fields that type carries.

Decisions:

  • nameOrId vs name: nameOrId wherever a user is named by text; raw name for avatar initials, filters, sorts and mapping. A custom Translations interpolating user.name now prints null for a user with no name; the guide says so.
  • toJson writes every field. Persistence stores the current user as OwnUser.toJson and reads it back with OwnUser.fromJson, so these fields can't use includeToJson: false the way ChannelModel and Member do.
  • User.topLevelFields lists only User's own keys. Keys only an own user carries are read by OwnUser; a key the SDK doesn't model stays in a v1 user's custom.
  • User/OwnUser keep their hand-written copyWith. freezed can't generate one for OwnUser while it takes User's fields through super-parameters; the group 09 plan records that the move must keep merge's rules. UpdateUsersResponse and UpdateUserPartialRequest had no v10 copyWith, so no audit applies.
  • Partial updates take the API's names, matching updateChannelPartial and updateMemberPartial. Hard renames, no deprecated aliases.
  • updateUser no longer sends role, teams or teamsRole. A client-side token never stored them.
  • Temporary adapter: the user.updated handler's JSON round trip goes when the WebSocket moves to v2 (README table).

Left out on purpose: OwnUser doesn't expose unread_count, total_unread_count_by_team or latest_hidden_channels, which the connection's me carries. No event keeps them current (the per-team counts arrive on only some events and never after a mark read), so exposing them would ship values that are right only after connecting. A v1-decoded own user keeps them in custom, like any key the SDK doesn't model, so passing currentUser to updateUser stores them as custom data, as v10 did; the migration guide says so. Per-team unread counts and the hidden-channels list are a follow-up feature: FLU-1015.

Known gaps, unchanged from v10: the user.updated handler still empties blockedUserIds (the event always sends []) and resets shadowBanned and banExpires, which the event doesn't carry.

Testing

  • melos run analyze clean; dart format clean; generate_plan.py --check reports 0 problems.
  • stream_chat 1,956 tests, stream_chat_persistence 314, stream_chat_flutter_core 385, stream_chat_localizations 36, all green. stream_chat_flutter (GITHUB_ACTIONS=true) has the same 40 golden failures as the base.
  • New client_update_users_test.dart, client_update_users_partial_test.dart (shared users_fixtures.dart, every mapped field set to a non-default value, so a dropped or swapped field fails) and client_user_updated_test.dart (privacy settings applied).
  • user_test.dart and own_user_test.dart rewritten for the new fields, nameOrId and merge (the values it clears and the ones it keeps); persistence mapper tests extended for the new columns; avatar tests for a user with no name and for an empty image.
  • Live on the demo app (restored afterwards):
    • an upsert on v2 stores no client state except the unread_count that currentUser.custom carries back (v10 stored about a dozen such keys and failed with a 413 for any user with many devices), and a partial set and unset with the generated explicit nulls work;
    • the user's own user.updated carries the new privacy settings after a privacy-only change, which doesn't move updated_at.

Screenshots / Videos

Not applicable: no visible UI change beyond the avatar placeholder for a user with no name.

🤖 Generated with Claude Code

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

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 43c76a9c-3740-4188-a188-adcb91a16896

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • 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.

VelikovPetar and others added 7 commits October 9, 2026 14:27
…refactor/flu-953_user_updates_openapi_migration
…refactor/flu-953_user_updates_openapi_migration
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…refactor/flu-953_user_updates_openapi_migration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e user fields of their own

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…penapi_migration' into refactor/flu-953_user_updates_openapi_migration

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

Copy link
Copy Markdown

⚠️ Database Entity Files Modified

The following database entity files have been modified in this PR:

packages/stream_chat_persistence/lib/src/entity/users.dart

📝 Remember to:

  1. ✅ Database schema version bumped to 1107.
  2. Update entity schema tests if necessary.

Note: This comment is automatically generated by the CI workflow.

@VelikovPetar VelikovPetar changed the title refactor(llc)!: route user updates through the generated client refactor(llc, ui)!: route user updates through the generated client Oct 16, 2026
VelikovPetar and others added 5 commits October 16, 2026 15:58
…Id's v10 fallback

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…elds out of OwnUser

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…refactor/flu-953_user_updates_openapi_migration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@VelikovPetar
VelikovPetar requested a review from a team October 19, 2026 09:21
@VelikovPetar
VelikovPetar marked this pull request as ready for review October 19, 2026 09:21
@codecov

codecov Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.17857% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.53%. Comparing base (2a10064) to head (c989726).

Files with missing lines Patch % Lines
packages/stream_chat/lib/src/core/models/user.dart 84.00% 4 Missing ⚠️
...eam_chat_flutter/lib/src/channel/channel_name.dart 0.00% 3 Missing ⚠️
...ckages/stream_chat/lib/src/core/models/member.dart 33.33% 2 Missing ⚠️
...t_flutter/lib/src/channel/stream_channel_name.dart 0.00% 2 Missing ⚠️
.../stream_chat_flutter/lib/src/utils/extensions.dart 75.00% 2 Missing ⚠️
...utter/lib/src/utils/message_preview_formatter.dart 50.00% 2 Missing ⚠️
..._gallery_preview/stream_media_gallery_preview.dart 0.00% 1 Missing ⚠️
...lib/src/message_input/stream_message_composer.dart 0.00% 1 Missing ⚠️
...h_scroll_view/stream_message_search_list_tile.dart 0.00% 1 Missing ⚠️
...l_view/user_scroll_view/stream_user_grid_tile.dart 0.00% 1 Missing ⚠️
... and 3 more
Additional details and impacted files
@@                                 Coverage Diff                                  @@
##           refactor/flu-937_user_blocking_openapi_migration    #3052      +/-   ##
====================================================================================
+ Coverage                                             80.44%   80.53%   +0.08%     
====================================================================================
  Files                                                   498      500       +2     
  Lines                                                 29615    29677      +62     
====================================================================================
+ Hits                                                  23825    23899      +74     
+ Misses                                                 5790     5778      -12     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

VelikovPetar and others added 2 commits October 20, 2026 10:29
…refactor/flu-953_user_updates_openapi_migration

# Conflicts:
#	migrations/v11-migration.md
#	packages/stream_chat/CHANGELOG.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@xsahil03x xsahil03x left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 8d6edc233. Analyze is clean in every package; the stream_chat (1,956), core (385), persistence (314) and localizations (36) tests pass; dart format reports nothing; and the UI suite fails only on local macOS goldens. Each suggestion was applied locally and analyzed before posting.

What I checked beyond the diff:

  • The name change: no user.name interpolation or display use is left in the UI, core, localizations or sample app; every one reads nameOrId. The remaining raw reads are the avatar initials and the sort, as intended.
  • Both user mappers are complete: UserResponseMapper maps all 17 fields of the generated UserResponse, and FullUserResponseMapper adds banExpires, invisible and shadowBanned. toRequest() sends exactly what the updateUser doc promises.
  • Reconnects don't send the me custom keys: the connection session keeps the user passed to connectUser (connection_manager.dart:145), not the server's me, so unread_count and friends never reach a connect request.
  • The user.updated adapter: it decodes every user.updated user as an OwnUser, including other users', which then sit in ClientState.users as OwnUser instances. Harmless today; worth keeping in mind until the v2 WebSocket move removes it.
  • OwnUser.merge keeps v10's rules through the constructor, and the schema bump (1100 + 7) is in place.

Comment thread packages/stream_chat/lib/src/client/client.dart Outdated
Comment thread packages/stream_chat/lib/src/client/client.dart Outdated
Comment thread packages/stream_chat/lib/src/core/models/request/update_user_partial_request.dart Outdated
Comment thread packages/stream_chat/lib/src/ws/events/event.dart Outdated
Comment thread packages/stream_chat/lib/src/core/models/user.dart

@xsahil03x xsahil03x left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two follow-ups from a deeper pass over the user models.

Comment thread migrations/v11-migration.md Outdated
);
}

/// Maps a generated [api.FullUserResponse] to a [User].

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question, not a blocker. Keeping Map<String, User> looks right: for a client-side token, the API clears devices, mutes, channel mutes, unread counts, invisible and privacySettings on every entry but the caller's (payload/update_users.go:194,239-256), so a User carrying them would show empty values for everyone else.

The caller's own entry is the one with real data, and this drops it, so a privacySettings or invisible change made through updateUser only reaches currentUser when user.updated arrives. Swift saves that entry into current-user state on success (CurrentUserUpdater.swift:70, saveCurrentUser(fullResponse:)), and Android maps all of it onto its single User. Should we merge it into state.currentUser here, or leave it as the additive follow-up the description mentions?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this out of the PR on purpose. v10 never applied the response to currentUser either, and the user.updated event still brings the privacy settings and invisible across, so nothing regresses. I didn't align it with Android and iOS here because I wanted to keep this PR to the endpoint migration and avoid changing how state is handled. Doing it properly also needs response mappers for Device, Mute and ChannelMute. Tracked in FLU-1045.

VelikovPetar and others added 6 commits October 22, 2026 09:04
Co-authored-by: Sahil Kumar <xdsahil@gmail.com>
Co-authored-by: Sahil Kumar <xdsahil@gmail.com>
Co-authored-by: Sahil Kumar <xdsahil@gmail.com>
Co-authored-by: Sahil Kumar <xdsahil@gmail.com>
Co-authored-by: Sahil Kumar <xdsahil@gmail.com>
Co-authored-by: Sahil Kumar <xdsahil@gmail.com>
VelikovPetar and others added 2 commits October 22, 2026 09:20
…penapi_migration' into refactor/flu-953_user_updates_openapi_migration

# Conflicts:
#	migrations/v11-migration.md
#	packages/stream_chat/CHANGELOG.md
#	packages/stream_chat/lib/src/core/models/user.dart
#	packages/stream_chat/lib/src/repository/mapper/users_mapper.dart
#	packages/stream_chat/lib/src/repository/users_repository.dart
#	sample_app/lib/widgets/message_info_sheet.dart
…tings in the handler

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

Copy link
Copy Markdown
Contributor Author

Good catch on the OwnUser instances in state.users: an OwnUser never equals a User with the same values, so it also defeated the no-op skip in updateUsers. Fixed in c989726: Event.fromJson no longer re-decodes the user, and the user.updated handler reads the privacy settings back through JSON only for the current user, so other users stay plain Users.

The round trip is temporary. It's only needed while events are decoded from v1 JSON, where User has no field for privacy settings. It's tagged TODO(openapi-migration) and listed in the README's temporary adapters. When the WebSocket moves to v2, the generated UserUpdatedEvent.user is mapped onto the current user once and the round trip goes away.

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.

2 participants