Repository navigation
refactor(llc, ui)!: route user updates through the generated client - #3052
VelikovPetar wants to merge 23 commits into
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
…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>
|
…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>
Codecov Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
…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
left a comment
There was a problem hiding this comment.
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
namechange: nouser.nameinterpolation or display use is left in the UI, core, localizations or sample app; every one readsnameOrId. The remaining raw reads are the avatar initials and the sort, as intended. - Both user mappers are complete:
UserResponseMappermaps all 17 fields of the generatedUserResponse, andFullUserResponseMapperaddsbanExpires,invisibleandshadowBanned.toRequest()sends exactly what theupdateUserdoc promises. - Reconnects don't send the
mecustom keys: the connection session keeps the user passed toconnectUser(connection_manager.dart:145), not the server'sme, sounread_countand friends never reach a connect request. - The
user.updatedadapter: it decodes everyuser.updateduser as anOwnUser, including other users', which then sit inClientState.usersasOwnUserinstances. Harmless today; worth keeping in mind until the v2 WebSocket move removes it. OwnUser.mergekeeps v10's rules through the constructor, and the schema bump (1100 + 7) is in place.
xsahil03x
left a comment
There was a problem hiding this comment.
Two follow-ups from a deeper pass over the user models.
| ); | ||
| } | ||
|
|
||
| /// Maps a generated [api.FullUserResponse] to a [User]. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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>
…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>
|
Good catch on the The round trip is temporary. It's only needed while events are decoded from v1 JSON, where |
Submit a pull request
Linear: FLU-953
Github Issue: -
CLA
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 andOwnUser'sextraDatatocustomand turns the values that lived in it into fields (README rule 9).User updates
StreamChatClient.updateUserandupdateUsersreturnResult<UpdateUsersResponse>instead of throwing.partialUpdateUserandpartialUpdateUsersare renamedupdateUserPartialandupdateUsersPartial, and returnResult<UpdateUsersResponse>;PartialUpdateUserRequestis renamedUpdateUserPartialRequest(freezed, notoJson).UpdateUsersResponsemoves tomodels/response/as a freezed class with a non-nullduration.updateUsersends only the user's name, image, language, visibility, custom data and, for an own user, privacy settings.UsersRepositorygains the two operations;users_mapper.dartgainsFullUserResponseMapper,UpdateUsersResponseMapperandUpdateUserPartialRequestMapper. The two methods are removed fromStreamChatApi.user.User custom data and fields
User.extraData→User.custom(also the constructor andcopyWitharguments).customis the API's custom data, unfiltered, in both directions;toJsonleaves out an entry named like one of the user's fields.User.nameandimageare nullable fields, stored as given.User.nameOrIdgives the name, or the id when the name is missing or empty, which is what v10'snamereturned.New and retyped fields:
Username,imageString?fields (were a non-nullnameand a nullableimagegetter)UsernameOrIdStringgetterUserdeactivatedAt,deletedAtDateTime?UsershadowBannedbool?UserblockedUserIdsList<String>(moved up fromOwnUser)OwnUser.fromUsercopies the user's fields instead of round-tripping through JSON, where a customidreplaced the user's id, soconnectUserconnected as the custom id.OwnUser.mergebuilds through the constructor: a nullable value the incoming user leaves out keeps the current one, exceptdeactivatedAt,deletedAtandshadowBanned, which are cleared.user.updated: for the current user, the handler rebuilds the event's user as anOwnUserthrough JSON, so the privacy settings their copy carries reachcurrentUser, as in v10. Other users stay plainUsers.ConnectUserDetails.toJsonleaves out a custom entry named like a user field, now thatcustomcan hold one.Persistence: users get
name,image,deactivated_at,deleted_at,shadow_banned,blocked_user_idscolumns andextra_databecomescustom; schema1100 + 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).StreamUserAvatarshows 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'simagedid. Filters and sorts read the rawname.DefaultApi.updateUsers(POST /api/v2/users)StreamChatClient.updateUser,updateUsersDefaultApi.updateUsersPartial(PATCH /api/v2/users)StreamChatClient.updateUserPartial,updateUsersPartialUpdateUsersResponseUpdateUsersResponse(ours, freezed)FullUserResponse,UserResponseUserUpdateUserPartialRequestUpdateUserPartialRequest(ours, freezed; wasPartialUpdateUserRequest)UserRequestUserbytoRequest()Why
The migration moves every endpoint onto the generated client, which returns a
Resultfor every call. The API calls the custom datacustom, and it types the user's name, image and the other values as fields; keeping them inextraDatalet 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
FullUserResponsemaps toUser; there is no public full-user type. The generated response answers aFullUserResponseper user: the profile plus the fields only that user can see (devices, mutes, privacy settings, unread counts, …).UpdateUsersResponsekeeps v10'sMap<String, User>and drops that private part. A client-side token can only update its own user, whose private fields are oncurrentUser. Mapping the caller's own entry to anOwnUserstays an additive option onceMute,ChannelMuteandPrivacySettingshave response mappers.Important
User.namechanges meaning;nameOrIdis the old getter.User.nameStringgetter overextraData['name']; the id when the name is missing or''String?field;nullwhen the user has no nameUser.nameOrId'': the oldnameUser.imageextraData['image'];nullwhen missing or''String?fieldname:argument, orextraData: {'name': …}name:argument only;custom: {'name': …}is just custom dataEverything that used the old
nameto name a user by text now callsnameOrId, so the UI reads the same. Code that relied onnamenever being empty (string interpolation,.isNotEmptychecks) compiles unchanged and printsnullfor a user with no name.Important
The
user.updatedhandler reads the current user's privacy settings back through JSON.Userdoesn't modelprivacy_settings, so on the current user's copy of the event they land incustom. Inside the existing current-user check, the handler rebuilds the user withOwnUser.fromJson(user.toJson()), which reads them intoprivacySettings(verified live, including after a privacy-only change). The round trip is safe becauseUser.toJsonleaves out custom keys named like a field. It is a temporary adapter, taggedTODO(openapi-migration)and listed in the README: when the WebSocket moves to v2, the generatedUserUpdatedEvent.user(UserResponsePrivacyFields) is mapped once and the handler applies only the fields that type carries.Decisions:
nameOrIdvsname:nameOrIdwherever a user is named by text; rawnamefor avatar initials, filters, sorts and mapping. A customTranslationsinterpolatinguser.namenow printsnullfor a user with no name; the guide says so.toJsonwrites every field. Persistence stores the current user asOwnUser.toJsonand reads it back withOwnUser.fromJson, so these fields can't useincludeToJson: falsethe wayChannelModelandMemberdo.User.topLevelFieldslists onlyUser's own keys. Keys only an own user carries are read byOwnUser; a key the SDK doesn't model stays in a v1 user'scustom.User/OwnUserkeep their hand-writtencopyWith. freezed can't generate one forOwnUserwhile it takesUser's fields through super-parameters; the group 09 plan records that the move must keepmerge's rules.UpdateUsersResponseandUpdateUserPartialRequesthad no v10copyWith, so no audit applies.updateChannelPartialandupdateMemberPartial. Hard renames, no deprecated aliases.updateUserno longer sendsrole,teamsorteamsRole. A client-side token never stored them.user.updatedhandler's JSON round trip goes when the WebSocket moves to v2 (README table).Left out on purpose:
OwnUserdoesn't exposeunread_count,total_unread_count_by_teamorlatest_hidden_channels, which the connection'smecarries. 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 incustom, like any key the SDK doesn't model, so passingcurrentUsertoupdateUserstores 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.updatedhandler still emptiesblockedUserIds(the event always sends[]) and resetsshadowBannedandbanExpires, which the event doesn't carry.Testing
melos run analyzeclean;dart formatclean;generate_plan.py --checkreports 0 problems.stream_chat1,956 tests,stream_chat_persistence314,stream_chat_flutter_core385,stream_chat_localizations36, all green.stream_chat_flutter(GITHUB_ACTIONS=true) has the same 40 golden failures as the base.client_update_users_test.dart,client_update_users_partial_test.dart(sharedusers_fixtures.dart, every mapped field set to a non-default value, so a dropped or swapped field fails) andclient_user_updated_test.dart(privacy settings applied).user_test.dartandown_user_test.dartrewritten for the new fields,nameOrIdandmerge(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.unread_countthatcurrentUser.customcarries 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 explicitnulls work;user.updatedcarries the new privacy settings after a privacy-only change, which doesn't moveupdated_at.Screenshots / Videos
Not applicable: no visible UI change beyond the avatar placeholder for a user with no name.
🤖 Generated with Claude Code