Skip to content

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

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

VelikovPetar wants to merge 11 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.

  • 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: the event's user is decoded as an OwnUser, so the privacy settings it carries for the current user reach currentUser, as in v10.

  • ConnectUserDetails.toJson no longer lets a custom entry overwrite a user field (a custom id replaced the real 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

Event.fromJson decodes a user.updated event's user a second time, as an OwnUser. User doesn't model privacy_settings, which the current user's copy of the event carries; v10 kept them in extraData and OwnUser.fromUser read them back through JSON. Re-decoding the user as an OwnUser keeps them, so a privacy change still reaches currentUser (verified live: the own copy carries the new settings, even after a privacy-only change). 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 re-decode in Event.fromJson 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. 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,946 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; 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 stores no client state on v2, 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

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.

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 3 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>

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