Skip to content

Add Static Location and Live Location Support #3531

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 118 commits into
base: develop
Choose a base branch
from

Conversation

nuno-vieira
Copy link
Member

@nuno-vieira nuno-vieira commented Dec 13, 2024

🔗 Issue Links

Resolves https://linear.app/stream/issue/IOS-578/location-attachment

🎯 Goal

Adds support for static and live location attachments in the Low-Level Client SDK.

The UI has been implemented in the Demo App to demonstrate how to use the new location APIs.

📝 Summary

New APIs:

  • ChatChannelController
    • sendStaticLocation() - Sends a static location message to the channel.
    • startLiveLocationSharing() - Starts a live location-sharing message in the channel.
  • ChatMessageController
    • partialUpdateMessage() - Updates the message partially. (It was missing from the SDK)
    • stopLiveLocationSharing() - Stops sharing the live location attachment if it has one.
  • CurrentChatUserController
    • updateLiveLocation() - Updates the location of all active live location messages for the current user.
    • loadActiveLiveLocationMessages() - Loads all active locations of the current user.
  • CurrentChatUserControllerDelegate
    • didStartSharingLiveLocation() - Notifies whenever the current user is sharing any live location.
    • didStopSharingLiveLocation() - Notifies whenever the current user stopped/expired all live locations.
    • didChangeActiveLiveLocationMessages() - Notifies whenever the active live location messages update.
  • Throttler
    • The Throttler was part of the UI SDK somehow, so it was moved to the LLC and made public like the Debouncer.
  • ChatMessage
    • sharedLocation - Returns the location if it has one, either live or static.

🛠 Implementation

The SDK at the moment only handles updating the location attachments. The location tracking should be provided by the App. Something like the LocationProvider in the Demo App should be implemented by the customer.

Creating a location attachment

In order to create a new message with a location attachment, the developer can use the ChannelController.sendStaticLocation() or the ChannelController.startSharingLiveLocation().

Sending location updates (Live Location)

The customer is responsible for sending new location updates to the SDK. This is done through the CurrentChatUserController.updateLiveLocation() method. This method will update all the current user's active location attachments. Internally, it uses a activeLiveLocationMessagesObserver that keeps track of the active location attachments of the current user. These changes are also available to the customer through the CurrentChatUserControllerDelegate to make it easier for the developer to know when it should track location updates and when it can turn them off.

Stopping live location attachment

To stop a live location attachment, it can be done through 2 methods:

  • ChatMessageController.stopLiveLocationSharing(): Directly tries to stop a live location attachment in the given message if it has an active location attachment.

Overall Data Flow

sequenceDiagram
    participant App
    participant LocationProvider
    participant SDK
    participant Backend

    Note over App,Backend: Start New Location Share
    App->>LocationProvider: getCurrentLocation()
    LocationProvider-->>App: Return current location
    App->>SDK: startLiveLocationSharing(location)
    SDK->>Backend: Create location message
    Backend-->>SDK: Confirm creation
    SDK->>App: onStartLiveLocationSharing()
    App->>LocationProvider: startMonitoring()

    Note over App,Backend: Location Updates
    LocationProvider->>LocationProvider: Monitor location changes
    LocationProvider->>SDK: didUpdateLocation(location)
    SDK->>Backend: updateLiveLocation
    Note over SDK: 3s Throttling

    Note over App,Backend: Stop Location Share
    App->>SDK: stopLiveLocationSharing()
    SDK->>Backend: Stop location sharing
    Backend-->>SDK: Confirm stop
    SDK->>App: onStopLiveLocationSharing()
    App->>LocationProvider: stopMonitoring()
Loading

🎨 Showcase

Static Live
static.mp4
live.mp4

🧪 Manual Testing Notes

Note: To simulate live location updates in the Simulator, with the Simulator selected, Go to Features > Location > City Bicycle Ride in the top bar of the Mac.

Static

  1. Open the Demo App
  2. Tap on Configuration
  3. Enable Location Attachments (Skip this step if Stream Developers scheme is being used)
  4. Open a Channel
  5. Tap on Attachments Icon
  6. Tap "Send Current Location"
  7. It should render an image of the map with a Pin
  8. Tapping the attachment should open the map with the Pin

Live

  1. Open the Demo App
  2. Tap on Configuration
  3. Enable Location Attachments (Skip this step if Stream Developers scheme is being used)
  4. Open a Channel
  5. Tap on Attachments Icon
  6. Tap "Share Live Location"
  7. It should render an image of the map with the avatar of the user
    • The image is static and does not update. It is generated when the channel is opened.
    • The avatar does not have the blue pulse animation. This is due to the fact that we reload the cell on every update, which would restart the animation.
  8. Tapping the attachment should open the map with the user's avatar, and it should follow his location.
  9. Tapping the "Stop Sharing" button should stop sharing the live location.

Make sure to test from the current user's perspective and the other user's perspective.

☑️ Contributor Checklist

  • I have signed the Stream CLA (required)
  • This change should be manually QAed
  • Changelog is updated with client-facing changes
  • Changelog is updated with new localization keys
  • New code is covered by unit tests
  • Comparison screenshots added for visual changes
  • Affected documentation updated (docusaurus, tutorial, CMS)

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added support for sending static and live location messages, including starting, updating, and stopping live location sharing.
    • Introduced partial message update functionality to update selected message fields without full replacement.
    • Added new UI components for shared and live locations: live status indicators, user avatars, map previews, control banners, and user annotations on maps.
    • Enhanced message actions and channel list previews to indicate location attachment status.
    • Integrated location permission handling, background location support, and a location provider for real-time updates.
    • Introduced live location lifecycle management with delegate callbacks and throttled updates.
  • Improvements

    • Enhanced user feedback and error handling for location permission and live sharing states.
    • Updated privacy usage descriptions and app configuration for location sharing.
    • Improved map snapshot caching and dynamic sizing for location previews.
    • Refined location attachment UI to differentiate live and static locations with distinct icons and texts.
    • Reorganized location-related code and dependencies for better maintainability.
  • Bug Fixes

    • Resolved issues with message actions filtering when location attachments are present.
  • Documentation

    • Updated changelog and in-app descriptions to include new location sharing capabilities.
  • Tests

    • Added extensive tests covering location sharing features, partial message updates, controller behaviors, and database persistence.

@nuno-vieira nuno-vieira added 🌐 SDK: StreamChat (LLC) Tasks related to the StreamChat LLC SDK ✅ Feature An issue or PR related to a feature labels Dec 18, 2024
Copy link
Contributor

@martinmitrevski martinmitrevski left a comment

Choose a reason for hiding this comment

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

Couldn't test a lot, since a had constant crashes (shared details on Slack). Let's figure that out first, then I will do another round.

@nuno-vieira nuno-vieira marked this pull request as ready for review January 3, 2025 17:40
@nuno-vieira nuno-vieira requested a review from a team as a code owner January 3, 2025 17:40
@nuno-vieira nuno-vieira changed the title [WIP] Add Static Location and Live Location Support Add Static Location and Live Location Support Jan 3, 2025
@nuno-vieira nuno-vieira force-pushed the add/location-attachments branch from 5e5a9ab to 70218c2 Compare January 3, 2025 22:01
Copy link

github-actions bot commented Jan 3, 2025

1 Warning
⚠️ Big PR

Generated by 🚫 Danger

@Stream-SDK-Bot
Copy link
Collaborator

Stream-SDK-Bot commented Jan 3, 2025

SDK Size

title develop branch diff status
StreamChat 7.2 MB 7.44 MB +244 KB 🟢
StreamChatUI 4.72 MB 4.78 MB +64 KB 🟢

Copy link
Contributor

@martinmitrevski martinmitrevski left a comment

Choose a reason for hiding this comment

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

Looks good! Left few comments, let me know what you think.

Copy link
Contributor

@martinmitrevski martinmitrevski left a comment

Choose a reason for hiding this comment

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

LGTM! ✅

@nuno-vieira nuno-vieira added the 🤞 Ready For QA A PR that is Ready for QA label Jan 9, 2025
@testableapple testableapple added 🧪 QAing 🟢 QAed A PR that was QAed and removed 🤞 Ready For QA A PR that is Ready for QA 🧪 QAing labels Jan 9, 2025
@nuno-vieira nuno-vieira force-pushed the add/location-attachments branch from 502d6b5 to f1a4fac Compare January 9, 2025 22:31
Copy link

coderabbitai bot commented Jun 2, 2025

## Walkthrough

This change introduces comprehensive support for location attachments in the StreamChat iOS SDK. It adds static and live location sharing, new data models, database schema updates, API endpoints, controller methods, UI components, and extensive tests. Previous custom location attachment code is removed and replaced with a more robust, integrated solution for sharing and managing user locations in chat messages.

## Changes

| File(s) / Group | Change Summary |
|---|---|
| `CHANGELOG.md` | Documents new location attachment features and partial message updates. |
| `DemoApp/Info.plist` | Adds location usage descriptions, background location mode, and reorganizes privacy keys. |
| `DemoApp/LocationProvider.swift` | Adds a singleton class for location permission and updates using `CLLocationManager`. |
| `DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift` | Enables location attachments by default. |
| `DemoApp/Screens/DemoAppTabBarController.swift` | Integrates live location sharing lifecycle and updates via delegate methods. |
| `DemoApp/StreamChat/Components/CustomAttachments/DemoAttachmentViewCatalog.swift` | Switches location detection to use new `sharedLocation` property. |
| `DemoApp/StreamChat/Components/CustomAttachments/DemoComposerVC.swift` | Replaces dummy locations with device location, adds static/live location actions, and permission handling. |
| `DemoApp/StreamChat/Components/CustomAttachments/DemoQuotedChatMessageView.swift` | Refactors preview logic to use `sharedLocation` and differentiate live/static. |
| `DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentPayload.swift`<br>`LocationAttachmentPayload+AttachmentViewProvider.swift` | Removes legacy custom location attachment types and preview provider. |
| `DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentSnapshotView.swift` | Refactors to support live location UI, avatar, stop sharing, and snapshot caching. |
| `DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentViewDelegate.swift` | Changes delegate protocol to use new `SharedLocation`, adds stop sharing, updates VC logic. |
| `DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentViewInjector.swift` | Refactors to use `SharedLocation` and adds tap handlers for stop sharing. |
| `DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationDetailViewController.swift` | Refactors to use `ChatMessageController`, adds live location tracking, banner, and animations. |
| `DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationSharingStatusView.swift`<br>`UserAnnotation.swift`<br>`UserAnnotationView.swift` | Adds new UI components for location sharing status and map annotations. |
| `DemoApp/StreamChat/Components/DemoChatChannelListItemView.swift` | Adds subtitle text for live/static location in channel list. |
| `DemoApp/StreamChat/Components/DemoChatMessageActionsVC.swift` | Hides edit action for messages with location attachments. |
| `DemoApp/StreamChat/StreamChatWrapper+DemoApp.swift` | Removes legacy location attachment injector registration. |
| `Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift` | Re-formats method signature. |
| `Sources/StreamChat/APIClient/Endpoints/MessageEndpoints.swift` | Adds partial update endpoint and support for updating attachments, text, extraData. |
| `Sources/StreamChat/APIClient/Endpoints/Payloads/IdentifiableModel.swift` | Adds `IdentifiableModel` conformance to `LocationDTO`. |
| `Sources/StreamChat/APIClient/Endpoints/Payloads/LocationPayloads.swift` | Adds new payload types for shared/live location. |
| `Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift` | Adds support for location property in message payloads. |
| `Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift`<br>`EndpointPath+OfflineRequest.swift` | Adds `.liveLocations` endpoint path. |
| `Sources/StreamChat/APIClient/Endpoints/LocationEndpoints.swift` | Adds endpoints for updating, stopping, and fetching live locations. |
| `Sources/StreamChat/Controllers/ChannelController/ChannelController.swift` | Adds methods for sending static/live location messages and error handling. |
| `Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift` | Adds live location observation, update, throttling, and delegate notifications. |
| `Sources/StreamChat/Controllers/MessageController/MessageController.swift` | Adds partial update, live location update/stop, and related errors. |
| `Sources/StreamChat/Database/DTOs/LocationDTO.swift` | Adds new Core Data entity and logic for location data. |
| `Sources/StreamChat/Database/DTOs/MessageDTO.swift` | Adds location property, fetch methods, and model conversion for location. |
| `Sources/StreamChat/Database/DatabaseSession.swift` | Adds location to message creation, new protocol for saving location, and updates session conformance. |
| `Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/.../contents` | Adds `LocationDTO` entity and relationship to `MessageDTO`. |
| `Sources/StreamChat/Models/ChatMessage.swift` | Adds `sharedLocation` property and updates initializer, equality, and copy methods. |
| `Sources/StreamChat/Models/DraftMessage.swift` | Sets `sharedLocation` to nil in `ChatMessage` init from draft. |
| `Sources/StreamChat/Models/Location/LocationInfo.swift`<br>`SharedLocation.swift` | Adds new models for location info and shared location. |
| `Sources/StreamChat/Models/UserInfo.swift` | Updates documentation comment. |
| `Sources/StreamChat/Repositories/MessageRepository.swift` | Adds method to fetch active live location messages for current user. |
| `Sources/StreamChat/Utils/Throttler.swift` | Makes `Throttler` public for throttling location updates. |
| `Sources/StreamChat/WebSocketClient/Events/MessageEvents.swift` | Adds `sharedLocation` to model mapping. |
| `Sources/StreamChat/Workers/ChannelUpdater.swift` | Adds location parameter to message creation. |
| `Sources/StreamChat/Workers/CurrentUserUpdater.swift` | Adds method to load active live locations from backend. |
| `Sources/StreamChat/Workers/MessageUpdater.swift` | Adds partial update, live location update/stop, and makes session extension public. |
| `StreamChat.xcodeproj/project.pbxproj` | Removes old files, adds new location-related files, updates package dependency. |
| `TestTools/StreamChatTestTools/...` | Updates mocks, test data, and spies to support new location features and partial update. |
| `Tests/StreamChatTests/...` | Adds extensive tests for location features, partial updates, and database integration. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant App (DemoApp)
    participant LocationProvider
    participant ChatChannelController
    participant MessageUpdater
    participant Backend

    User->>App: Taps "Share Location"
    App->>LocationProvider: getCurrentLocation()
    LocationProvider-->>App: CLLocation (lat, lon)
    App->>ChatChannelController: sendStaticLocation(location, ...)
    ChatChannelController->>MessageUpdater: createNewMessage with location
    MessageUpdater->>Backend: Send message with location payload
    Backend-->>MessageUpdater: Success/Failure
    MessageUpdater-->>ChatChannelController: Result
    ChatChannelController-->>App: Completion handler
    App-->>User: Shows message in chat
sequenceDiagram
    participant User
    participant App (DemoApp)
    participant LocationProvider
    participant CurrentChatUserController
    participant Backend

    User->>App: Starts live location sharing
    App->>LocationProvider: startMonitoringLocation()
    loop While sharing active
        LocationProvider-->>App: didUpdateLocation(CLLocation)
        App->>CurrentChatUserController: updateLiveLocation(LocationInfo)
        CurrentChatUserController->>Backend: updateLiveLocation API
        Backend-->>CurrentChatUserController: Ack
    end
    User->>App: Stops live location sharing
    App->>CurrentChatUserController: stopLiveLocationSharing()
    CurrentChatUserController->>Backend: stopLiveLocation API
    Backend-->>CurrentChatUserController: Ack
    App-->>User: Shows live location ended
Loading

Assessment against linked issues

Objective Addressed Explanation
Implement location attachment feature (static and live location sharing, API integration, UI, data models, controller methods, database support, and tests) [IOS-578]
Remove legacy custom location attachment code and migrate to new integrated approach [IOS-578]

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Update of documentation comment in Sources/StreamChat/Models/UserInfo.swift The change is a minor doc comment update and not related to the location attachment objective, but it is trivial and does not affect functionality.
Formatting change only in TestTools/StreamChatTestTools/TestData/DummyData/MessageAttachmentPayload.swift This is a copyright/formatting-only update and unrelated to the location feature.

Suggested reviewers

  • laevandus

Poem

🐇
A rabbit hopped with GPS in paw,
Now chat can share just where you are!
Live or static, maps appear,
With avatars and status clear.
From code to UI, tests abound—
Location magic now is found!
🗺️✨


<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=GetStream/stream-chat-swift&utm_content=3531):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 24

🔭 Outside diff range comments (1)
Sources/StreamChat/Workers/MessageUpdater.swift (1)

1033-1052: 💡 Verification agent

🧩 Analysis chain

Extension visibility change requires verification.

The DatabaseSession extension was changed from private to public, which expands the API surface. Ensure this change is intentional and that the messageEditableByCurrentUser method should be publicly accessible.


🏁 Script executed:

#!/bin/bash
# Description: Check if the messageEditableByCurrentUser method is used outside this file
# Expected: Find usages to confirm the visibility change is necessary

rg "messageEditableByCurrentUser" --type swift -A 2 -B 2

Length of output: 3204


Review Extension Visibility: Revert to internal

The messageEditableByCurrentUser(_:) helper is only used within the StreamChat module (in MessageUpdater.swift and MessageController.swift). Making it public unnecessarily expands the library’s public API. Please revert its access level to internal (the default) unless there’s a specific need for external clients to call it.

• Sources/StreamChat/Workers/MessageUpdater.swift
– Declaration of messageEditableByCurrentUser
• Sources/StreamChat/Controllers/MessageController/MessageController.swift
– Calls to session.messageEditableByCurrentUser(self.messageId)

Suggested diff:

- public extension DatabaseSession {
+ extension DatabaseSession {
     /// Returns the message if it can be edited by the current user...
     func messageEditableByCurrentUser(_ messageId: MessageId) throws -> MessageDTO {
         …
     }
 }
🧹 Nitpick comments (24)
Tests/StreamChatTests/Database/DTOs/MessageDTO_Tests.swift (1)

4195-4273: Well-structured test with minor documentation issue.

This test effectively covers the loadActiveLiveLocationMessages functionality with comprehensive test scenarios. The setup creates appropriate test data covering different users, channels, and active states, and the assertions properly validate both global and channel-specific filtering.

However, there's a misleading comment on line 4220:

(.unique, currentUserId, channel1Id, false) // Current user, channel 1, no location

The comment says "no location" but the code still creates a live location attachment for this message - it's just inactive (with stoppedSharing: true). The comment should be corrected to reflect this.

Apply this diff to fix the misleading comment:

-            (.unique, currentUserId, channel1Id, false) // Current user, channel 1, no location
+            (.unique, currentUserId, channel1Id, false) // Current user, channel 1, inactive
Tests/StreamChatTests/Models/Attachments/LiveLocationAttachmentPayload_Tests.swift (2)

10-32: Enhance test to verify true default value behavior.

The test name suggests testing default values, but stoppedSharing is explicitly provided in the JSON. Consider testing what happens when stoppedSharing is omitted to verify the model's default value handling.

Consider adding a separate test for truly default values:

+    func test_decodingWithMissingStoppedSharing() throws {
+        let latitude: Double = 51.5074
+        let longitude: Double = -0.1278
+        
+        let json = """
+        {
+            "latitude": \(latitude),
+            "longitude": \(longitude)
+        }
+        """.data(using: .utf8)!
+        
+        let payload = try JSONDecoder.stream.decode(LiveLocationAttachmentPayload.self, from: json)
+        
+        XCTAssertEqual(payload.latitude, latitude)
+        XCTAssertEqual(payload.longitude, longitude)
+        // Verify default value for stoppedSharing when not provided
+        XCTAssertEqual(payload.stoppedSharing, false) // or true, depending on expected default
+    }

34-56: Consider testing stoppedSharing in the extra data test.

The extra data test could be more comprehensive by including the stoppedSharing field to ensure it works correctly alongside extra data.

     func test_decodingExtraData() throws {
         // Create attachment field values
         let latitude: Double = 51.5074
         let longitude: Double = -0.1278
+        let stoppedSharing = false
         let locationName: String = .unique
         
         // Create JSON with the given values
         let json = """
         {
             "latitude": \(latitude),
             "longitude": \(longitude),
+            "stopped_sharing": \(stoppedSharing),
             "locationName": "\(locationName)"
         }
         """.data(using: .utf8)!
         
         // Decode attachment from JSON
         let payload = try JSONDecoder.stream.decode(LiveLocationAttachmentPayload.self, from: json)
         
         // Assert values are decoded correctly
         XCTAssertEqual(payload.latitude, latitude)
         XCTAssertEqual(payload.longitude, longitude)
+        XCTAssertEqual(payload.stoppedSharing, stoppedSharing)
         XCTAssertEqual(payload.extraData?["locationName"]?.stringValue, locationName)
     }
Tests/StreamChatTests/Repositories/MessageRepository_Tests.swift (1)

703-724: Strengthen failure-path assertions

The test correctly checks that an error is emitted when the current user is missing, but it currently only verifies the error’s type.
It doesn’t assert that the Result itself is a failure, which means a future refactor that accidentally returns a success value containing an error‐like payload could still pass this test.

-        repository.getCurrentUserActiveLiveLocationMessages(for: cid) { result in
-            if case .failure(let error) = result {
-                receivedError = error
-            }
+        repository.getCurrentUserActiveLiveLocationMessages(for: cid) { result in
+            guard case .failure(let error) = result else {
+                XCTFail("Expected failure, got \(result)"); return
+            }
+            receivedError = error

This small change hardens the test against false–positives.

DemoApp/StreamChat/Components/CustomAttachments/DemoComposerVC.swift (1)

72-87: Present permission alert on the correct queue

showLocationPermissionAlert() is called from potentially arbitrary threads.
Wrap the present call in a main-queue dispatch to prevent runtime warnings:

-        present(alert, animated: true)
+        DispatchQueue.main.async {
+            self.present(alert, animated: true)
+        }

(Alternatively, incorporate this into the previous refactor.)

Tests/StreamChatTests/Workers/MessageUpdater_Tests.swift (1)

2977-3095: Excellent test coverage for the new updatePartialMessage functionality!

The new test suite provides comprehensive coverage for the partial message update feature with well-structured tests that follow existing patterns in the codebase. The three test methods effectively cover:

  1. API Contract Verification - Ensures correct endpoint and payload formatting
  2. Error Handling - Validates proper network error propagation
  3. Data Persistence - Confirms successful database updates

The tests are well-organized and use appropriate assertion patterns consistent with the existing test infrastructure.

Minor suggestion for potential improvement:

The attachment payload conversion logic (lines 2987-2996) could potentially be extracted to a helper method if this pattern is reused elsewhere, but it's acceptable as-is since it clearly demonstrates the expected transformation for this specific test case.

Consider extracting the attachment conversion logic to a helper method if similar conversions are needed in other tests:

+    private func convertAttachmentsToMessagePayloads(_ attachments: [AnyAttachmentPayload]) -> [MessageAttachmentPayload] {
+        return attachments.compactMap { attachment in
+            guard let payloadData = try? JSONEncoder.default.encode(attachment.payload),
+                  let payloadRawJSON = try? JSONDecoder.default.decode(RawJSON.self, from: payloadData) else {
+                return nil
+            }
+            return MessageAttachmentPayload(
+                type: attachment.type,
+                payload: payloadRawJSON
+            )
+        }
+    }

This would simplify the test and make the conversion logic reusable across other test methods.

DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/UserAnnotationView.swift (2)

46-76: Consider animation performance optimization.

The pulsing animation creates new layers each time startPulsingAnimation() is called. While the guard statement prevents duplicate layers, consider optimizing for scenarios where animations start/stop frequently during live location updates.

Consider reusing the pulse layer instead of recreating it:

 func startPulsingAnimation() {
-    guard pulseLayer == nil else {
-        return
-    }
-    let pulseLayer = CALayer()
+    if let existingLayer = pulseLayer {
+        existingLayer.removeAllAnimations()
+    } else {
+        let pulseLayer = CALayer()
+        // ... setup code ...
+        self.pulseLayer = pulseLayer
+    }
     // ... animation setup ...
 }

20-34: Consider making avatar size configurable.

The hardcoded size of 40x40 points works well for most use cases, but consider making it configurable for different map zoom levels or accessibility requirements.

-private var size: CGSize = .init(width: 40, height: 40)
+private var avatarSize: CGSize

+init(annotation: MKAnnotation?, reuseIdentifier: String?, size: CGSize = .init(width: 40, height: 40)) {
+    self.avatarSize = size
     // ... rest of init ...
+    frame = CGRect(x: 0, y: 0, width: avatarSize.width, height: avatarSize.height)
+    layer.cornerRadius = avatarSize.width / 2
StreamChat.xcodeproj/project.pbxproj (3)

1567-1568: Remove duplicate SharedLocation.swift build entries
There are two identical PBXBuildFile entries for SharedLocation.swift. Please remove one to avoid duplication in the build phase.


1671-1672: Remove duplicate LocationDTO.swift build entries
Duplicate build entries for LocationDTO.swift appear in this section. Keeping a single entry will prevent potential build warnings.


1838-1842: Remove duplicate entries for LocationInfo.swift and Throttler.swift
This hunk introduces duplicate PBXBuildFile entries for LocationInfo.swift (1838–1839) and Throttler.swift (1841–1842). Please remove the redundant lines to clean up the build phase.

DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentViewDelegate.swift (2)

20-26: Solid message controller creation approach.

The implementation correctly creates a message controller for the specific location context, enabling real-time updates in the detail view. Consider adding error handling for cases where the message controller creation might fail.


34-44: Consider extracting device-specific presentation logic.

The presentation logic handling iPad vs other devices could be extracted to a reusable utility method for better maintainability.

Consider creating a utility extension:

private extension UIViewController {
    func presentViewController(_ viewController: UIViewController, animated: Bool = true) {
        if UIDevice.current.userInterfaceIdiom == .pad {
            let nav = UINavigationController(rootViewController: viewController)
            navigationController?.present(nav, animated: animated)
        } else {
            navigationController?.pushViewController(viewController, animated: animated)
        }
    }
}

Then simplify the method:

 private func showDetailViewController(messageController: ChatMessageController) {
     let mapViewController = LocationDetailViewController(
         messageController: messageController
     )
-    if UIDevice.current.userInterfaceIdiom == .pad {
-        let nav = UINavigationController(rootViewController: mapViewController)
-        navigationController?.present(nav, animated: true)
-        return
-    }
-    navigationController?.pushViewController(mapViewController, animated: true)
+    presentViewController(mapViewController)
 }
Sources/StreamChat/APIClient/Endpoints/MessageEndpoints.swift (1)

110-113: Remove redundant = nil initialisation

unset is already an optional; explicitly initialising it with nil is unnecessary and triggers Swift-Lint’s redundant_optional_initialization warning.

-    var unset: [String]? = nil
+    var unset: [String]?
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 112-112: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

Tests/StreamChatTests/Controllers/MessageController/MessageController_Tests.swift (1)

2692-2750: Test names don’t match the scenario they verify

test_updateLiveLocation_whenNoLiveLocationAttachment… supplies an attachment whose stoppedSharing flag is true (i.e. already stopped), while
test_updateLiveLocation_whenLiveLocationHasAlreadyStopped… supplies no live-location attachment at all.

Although the assertions are correct, the misleading names make future maintenance error-prone.
Consider swapping the names (or fixing the fixtures) so that the intent of each test is self-documenting.

Sources/StreamChat/Database/DTOs/LocationDTO.swift (1)

45-46: Unnecessary sort descriptor harms fetch-by-ID performance

The fetch uses messageId == %@, which is fully selective.
Adding sortDescriptors = [ ... createdAt ] forces Core Data to create an extra ORDER BY, possibly preventing index usage.

-        request.sortDescriptors = [NSSortDescriptor(keyPath: \LocationDTO.message.createdAt, ascending: false)]
+        request.sortDescriptors = []

Removing the descriptor speeds up single-row fetches and avoids pointless work.

Sources/StreamChat/Controllers/MessageController/MessageController.swift (1)

969-984: Same attachment-clearing risk when stopping live-location sharing

stopLiveLocationSharing also transmits attachments: [], which could unintentionally remove the attachment rather than just updating its endAt field. Consider passing nil or using unset for the specific keys you actually want cleared.

Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift (2)

46-46: Remember to release the mock after cleanUp()

messageUpdater is cleaned up but still referenced by env.
Consider also null-ing the reference ( env?.messageUpdater = nil ) to make sure ARC can reclaim it immediately and avoid a (very small) risk of accidental reuse in later tests.


6160-6186: TestEnvironment – ensure the extra mock is released

messageUpdater was added to the environment but is not set to nil in cleanUp().
Just like the other mocks, consider:

func cleanUp() {
    messageUpdater = nil
    // existing clean-up calls...
}

Doing so avoids hidden strong-reference cycles should the mock capture closures.

DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentSnapshotView.swift (2)

99-104: Rename misleading selector handleTapOnWorkoutAttachment
The view shows a location snapshot, but the selector still mentions “Workout”. Renaming improves readability and future discoverability.

-    @objc func handleTapOnWorkoutAttachment() {
+    @objc func handleTapOnLocationAttachment() {

and update the target registration accordingly.

Also applies to: 143-146


202-208: Live-location snapshots are cached forever → stale thumbnails
getCachedSnapshot() is consulted even for live locations, so once a snapshot is cached the map never refreshes while the user moves. Consider:

  • Skipping the cache when content.isLive == true, or
  • Keying the cache with latitude/longitude (rounded) as well as messageId, or
  • Adding a TTL to the cache entry.

This prevents outdated location previews.

Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (1)

863-869: Empty text parameter sent as "" – is that intended?
When text is nil, the method forces text ?? "". If the backend differentiates between “no text” and “empty string”, this could create empty bubbles or violate validation rules. Consider making text optional in createNewMessage or omitting the field entirely when it’s not provided.

DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationDetailViewController.swift (1)

115-121: Animation length exceeds update interval → janky motion
The live-location is refreshed every ~3 s but the pin animation lasts 5 s. This means multiple animations overlap, producing visible stutter on long sessions. Either reduce the duration to the update cadence or cancel any running animation before starting a new one.

UIView.animate(withDuration: 3, ) {  }

or store the current animator and call stopAnimation(true) before re-creating it.

Sources/StreamChat/Database/DTOs/MessageDTO.swift (1)

774-786: Duplicate user lookup & slight cleanliness

Inside the createNewMessage flow you already have currentUserDTO.
Using currentUser?.currentDevice?.id re-queries CoreData and shadows the earlier variable.

-        if let location, let deviceId = currentUser?.currentDevice?.id {
+        if let location, let deviceId = currentUserDTO.currentDevice?.id {

This avoids an extra fetch and keeps naming consistent.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 29d6225 and 58653d8.

📒 Files selected for processing (54)
  • CHANGELOG.md (1 hunks)
  • DemoApp/Info.plist (2 hunks)
  • DemoApp/LocationProvider.swift (1 hunks)
  • DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift (1 hunks)
  • DemoApp/Screens/DemoAppTabBarController.swift (3 hunks)
  • DemoApp/StreamChat/Components/CustomAttachments/DemoAttachmentViewCatalog.swift (1 hunks)
  • DemoApp/StreamChat/Components/CustomAttachments/DemoComposerVC.swift (1 hunks)
  • DemoApp/StreamChat/Components/CustomAttachments/DemoQuotedChatMessageView.swift (1 hunks)
  • DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentPayload+AttachmentViewProvider.swift (0 hunks)
  • DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentPayload.swift (0 hunks)
  • DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentSnapshotView.swift (4 hunks)
  • DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentViewDelegate.swift (1 hunks)
  • DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentViewInjector.swift (1 hunks)
  • DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationDetailViewController.swift (2 hunks)
  • DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationSharingStatusView.swift (1 hunks)
  • DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/UserAnnotation.swift (1 hunks)
  • DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/UserAnnotationView.swift (1 hunks)
  • DemoApp/StreamChat/Components/DemoChatChannelListItemView.swift (1 hunks)
  • DemoApp/StreamChat/Components/DemoChatMessageActionsVC.swift (1 hunks)
  • DemoApp/StreamChat/StreamChatWrapper+DemoApp.swift (0 hunks)
  • Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift (1 hunks)
  • Sources/StreamChat/APIClient/Endpoints/MessageEndpoints.swift (2 hunks)
  • Sources/StreamChat/APIClient/Endpoints/Payloads/IdentifiableModel.swift (1 hunks)
  • Sources/StreamChat/APIClient/Endpoints/Payloads/LocationPayloads.swift (1 hunks)
  • Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift (9 hunks)
  • Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (7 hunks)
  • Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift (8 hunks)
  • Sources/StreamChat/Controllers/MessageController/MessageController.swift (7 hunks)
  • Sources/StreamChat/Database/DTOs/LocationDTO.swift (1 hunks)
  • Sources/StreamChat/Database/DTOs/MessageDTO.swift (10 hunks)
  • Sources/StreamChat/Database/DatabaseSession.swift (4 hunks)
  • Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents (2 hunks)
  • Sources/StreamChat/Models/ChatMessage.swift (4 hunks)
  • Sources/StreamChat/Models/DraftMessage.swift (1 hunks)
  • Sources/StreamChat/Models/Location/LocationInfo.swift (1 hunks)
  • Sources/StreamChat/Models/Location/SharedLocation.swift (1 hunks)
  • Sources/StreamChat/Models/UserInfo.swift (1 hunks)
  • Sources/StreamChat/Repositories/MessageRepository.swift (1 hunks)
  • Sources/StreamChat/Utils/Throttler.swift (3 hunks)
  • Sources/StreamChat/WebSocketClient/Events/MessageEvents.swift (1 hunks)
  • Sources/StreamChat/Workers/ChannelUpdater.swift (3 hunks)
  • Sources/StreamChat/Workers/MessageUpdater.swift (27 hunks)
  • StreamChat.xcodeproj/project.pbxproj (36 hunks)
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/MessageUpdater_Mock.swift (3 hunks)
  • TestTools/StreamChatTestTools/TestData/DummyData/MessageAttachmentPayload.swift (2 hunks)
  • Tests/StreamChatTests/APIClient/Endpoints/MessageEndpoints_Tests.swift (1 hunks)
  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift (4 hunks)
  • Tests/StreamChatTests/Controllers/MessageController/MessageController_Tests.swift (1 hunks)
  • Tests/StreamChatTests/Database/DTOs/MessageDTO_Tests.swift (1 hunks)
  • Tests/StreamChatTests/Models/Attachments/LiveLocationAttachmentPayload_Tests.swift (1 hunks)
  • Tests/StreamChatTests/Models/Attachments/StaticLocationAttachmentPayload_Tests.swift (1 hunks)
  • Tests/StreamChatTests/Models/ChatMessage_Tests.swift (1 hunks)
  • Tests/StreamChatTests/Repositories/MessageRepository_Tests.swift (1 hunks)
  • Tests/StreamChatTests/Workers/MessageUpdater_Tests.swift (1 hunks)
💤 Files with no reviewable changes (3)
  • DemoApp/StreamChat/StreamChatWrapper+DemoApp.swift
  • DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentPayload+AttachmentViewProvider.swift
  • DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentPayload.swift
🧰 Additional context used
🧬 Code Graph Analysis (7)
Tests/StreamChatTests/Models/Attachments/StaticLocationAttachmentPayload_Tests.swift (2)
Tests/StreamChatTests/Models/Attachments/LiveLocationAttachmentPayload_Tests.swift (3)
  • test_decodingDefaultValues (10-32)
  • test_decodingExtraData (34-56)
  • test_encoding (58-76)
TestTools/StreamChatTestTools/Extensions/Unique/URL+Unique.swift (1)
  • unique (9-11)
DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationSharingStatusView.swift (2)
DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentSnapshotView.swift (1)
  • setUpLayout (112-141)
Sources/StreamChatUI/ViewContainerBuilder/ViewContainerBuilder.swift (1)
  • HContainer (96-109)
Tests/StreamChatTests/Models/Attachments/LiveLocationAttachmentPayload_Tests.swift (2)
Tests/StreamChatTests/Models/Attachments/StaticLocationAttachmentPayload_Tests.swift (3)
  • test_decodingDefaultValues (10-29)
  • test_decodingExtraData (31-53)
  • test_encoding (55-71)
TestTools/StreamChatTestTools/Extensions/Unique/URL+Unique.swift (1)
  • unique (9-11)
Sources/StreamChat/Models/Location/SharedLocation.swift (1)
Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (1)
  • messageId (2089-2091)
Tests/StreamChatTests/APIClient/Endpoints/MessageEndpoints_Tests.swift (4)
TestTools/StreamChatTestTools/Extensions/Unique/URL+Unique.swift (1)
  • unique (9-11)
Sources/StreamChat/Controllers/MessageController/MessageController.swift (2)
  • editMessage (272-299)
  • partialUpdateMessage (311-329)
Sources/StreamChat/Workers/MessageUpdater.swift (2)
  • editMessage (108-182)
  • editMessage (1173-1191)
Sources/StreamChat/APIClient/Endpoints/MessageEndpoints.swift (2)
  • editMessage (28-40)
  • partialUpdateMessage (53-62)
Sources/StreamChat/Database/DTOs/LocationDTO.swift (4)
Sources/StreamChat/Database/DTOs/MessageDTO.swift (4)
  • loadOrCreate (491-506)
  • load (452-471)
  • load (487-489)
  • asModel (1585-1585)
Sources/StreamChat/APIClient/Endpoints/Payloads/IdentifiableModel.swift (1)
  • model (14-17)
Sources/StreamChat/Utils/Database/NSManagedObject+Extensions.swift (2)
  • insertNewObject (14-22)
  • applyPrefetchingState (52-56)
Sources/StreamChat/Database/DTOs/NSManagedObject+Validation.swift (1)
  • isNotDeleted (19-22)
Sources/StreamChat/Database/DatabaseSession.swift (1)
Sources/StreamChat/Database/DTOs/LocationDTO.swift (1)
  • saveLocation (67-82)
🪛 SwiftLint (0.57.0)
Sources/StreamChat/APIClient/Endpoints/MessageEndpoints.swift

[Warning] 112-112: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

🔇 Additional comments (111)
Sources/StreamChat/Models/UserInfo.swift (1)

7-7: Documentation clarification looks good.

The revised comment is clearer and aligns well with the existing model documentation style.

Sources/StreamChat/Models/DraftMessage.swift (1)

165-165: LGTM! Proper initialization of the new location property.

The addition of sharedLocation = nil is appropriate when creating a ChatMessage from a DraftMessage, as draft messages wouldn't contain location data. This initialization is consistent with the new location sharing functionality being introduced.

Sources/StreamChat/WebSocketClient/Events/MessageEvents.swift (1)

297-298: LGTM! Consistent property initialization in payload mapping.

The addition of sharedLocation: nil properly initializes the new location property when mapping MessagePayload to ChatMessage. This is consistent with the other property initializations in this workaround method for deleted messages.

DemoApp/Screens/AppConfigViewController/AppConfigViewController.swift (1)

52-52: LGTM! Appropriate default configuration for demonstrating new features.

Enabling isLocationAttachmentsEnabled by default makes sense for showcasing the new location sharing functionality. This change allows users to immediately explore the location features in the demo app.

Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift (1)

138-143: LGTM! Good formatting improvement for readability.

The multi-line formatting of the sendMessage function signature improves code readability without any functional changes. This is a good practice for functions with multiple parameters.

DemoApp/StreamChat/Components/CustomAttachments/DemoAttachmentViewCatalog.swift (2)

11-11: LGTM! Clean migration to the new location attachment model.

The change from checking attachmentCounts[.location] to message.sharedLocation != nil is a clean migration that aligns with the new unified location attachment model introduced in this PR. This approach is more direct and efficient than counting attachments.


11-11: LGTM! Correctly migrates to the new location detection approach.

The change from checking attachmentCounts[.location] to message.sharedLocation != nil aligns with the migration to the new shared location model mentioned in the PR summary. This approach is cleaner and more direct for detecting location attachments.

Sources/StreamChat/Models/Location/LocationInfo.swift (2)

7-21: LGTM! Well-designed foundational data structure.

The LocationInfo struct is clean, simple, and appropriately designed as a foundational data type for location features. The public API design with clear property names and straightforward initializer makes it easy to use.


7-21: LGTM! Clean and well-documented coordinate data structure.

The LocationInfo struct is well-implemented with:

  • Proper documentation for all properties
  • Public accessibility for SDK consumers
  • Simple, focused design for representing geographic coordinates
  • Clear initializer

This serves as a solid foundation for location-based features.

DemoApp/StreamChat/Components/DemoChatMessageActionsVC.swift (2)

29-32: LGTM! Sensible restriction on editing location messages.

Preventing message editing when location attachments are present makes good UX sense, as edited location messages could lose their contextual meaning. The implementation correctly identifies location messages and filters out edit actions.


29-32: LGTM! Appropriate business logic for location messages.

Preventing editing of messages with location attachments is a sensible UX decision. Location messages, especially live location sharing, shouldn't typically be editable as this could disrupt the location sharing functionality. The implementation correctly uses the new sharedLocation property and cleanly filters out edit actions.

Sources/StreamChat/APIClient/Endpoints/Payloads/IdentifiableModel.swift (2)

102-106: LGTM! Standard conformance implementation.

The LocationDTO conformance to IdentifiableModel follows the established pattern used by other DTOs in the file. Using messageId as the identifier is appropriate since location data is tied to specific chat messages.


102-106: LGTM! Proper integration of LocationDTO into the identification system.

The LocationDTO extension correctly implements the IdentifiableModel protocol:

  • Uses messageId as the identifier, which appropriately links location data to messages
  • Follows the established pattern used by other DTO extensions in the file
  • Maintains consistency with the existing codebase architecture

This enables proper identification and retrieval of location data objects within the database layer.

Tests/StreamChatTests/APIClient/Endpoints/MessageEndpoints_Tests.swift (1)

193-215: LGTM! Well-structured test for the new partial update endpoint.

The test correctly validates the endpoint construction with appropriate test data including set, unset, and skipEnrichUrl parameters. The assertions properly verify both the endpoint equality and path string.

Sources/StreamChat/Workers/ChannelUpdater.swift (3)

381-381: Good addition of location parameter for message creation.

The optional location parameter follows the existing pattern of other optional parameters in the method signature and enables location data to be associated with new messages.


405-405: Correct parameter forwarding to database layer.

The location parameter is properly passed through to the database write operation, maintaining consistency with the method signature.


1147-1152: Well-designed location data model.

The NewLocationInfo struct provides a clean interface for location data with:

  • Required latitude and longitude coordinates as Double
  • Optional endAt date for live location expiration
  • Appropriate naming and structure
Sources/StreamChat/Models/ChatMessage.swift (4)

181-182: Proper addition of location property to message model.

The sharedLocation property is correctly declared as optional and follows the existing documentation pattern with a clear description.


224-224: Initializer parameter correctly added.

The sharedLocation parameter is appropriately placed in the initializer parameter list, maintaining consistency with the property order.


266-266: Property assignment in initializer is correct.

The sharedLocation property is properly assigned from the initializer parameter.


314-315: Replacing method correctly preserves location data.

The sharedLocation property is properly included in the replacing method, ensuring location data is preserved when creating modified message instances.

DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationSharingStatusView.swift (4)

9-16: Well-structured label component with proper theming.

The statusLabel is correctly configured with:

  • Proper auto layout setup
  • Theme-aware font and color
  • Clean lazy initialization

18-34: Good use of system images for location status.

The image configuration properly uses:

  • Appropriate system symbols (location.fill and location.slash.fill)
  • Consistent symbol scale (.medium)
  • Proper image view setup with content mode

36-45: Clean layout implementation using container helpers.

The layout setup correctly:

  • Uses HContainer for horizontal arrangement
  • Applies appropriate spacing and alignment
  • Sets proper constraints for icon size
  • Embeds the container properly

47-54: Effective status update method with proper theming.

The updateStatus method provides:

  • Clear text labels for both states
  • Appropriate icon switching
  • Theme-aware color changes (accent for active, subtitle for inactive)
  • Simple boolean parameter for state management
DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/UserAnnotation.swift (1)

9-18: LGTM! Clean MKAnnotation implementation.

The implementation correctly uses the dynamic keyword for the coordinate property, which is essential for MapKit's KVO-based coordinate change notifications. The class structure is simple and follows standard MKAnnotation patterns.

DemoApp/StreamChat/Components/DemoChatChannelListItemView.swift (1)

9-22: LGTM! Well-structured location preview handling.

The implementation correctly:

  • Handles both live and static location previews
  • Uses appropriate conditional logic to check for shared locations
  • Leverages existing helper methods for consistent formatting
  • Provides proper fallback to superclass implementation

The code follows established patterns in the codebase and integrates well with the location sharing feature.

Tests/StreamChatTests/Models/ChatMessage_Tests.swift (1)

396-527: LGTM! Comprehensive test coverage for location attachments.

The test implementation provides excellent coverage:

  • Tests both empty and populated attachment scenarios
  • Verifies proper filtering of static vs live location attachments
  • Uses realistic mock data with proper coordinate values
  • Employs Set comparison for ID verification to avoid order dependency
  • Follows established testing patterns in the codebase

The tests properly validate the new computed properties staticLocationAttachments and liveLocationAttachments on ChatMessage.

Sources/StreamChat/Repositories/MessageRepository.swift (1)

302-325: LGTM! Well-implemented repository method for live location queries.

The implementation correctly:

  • Uses backgroundReadOnlyContext for read-only database operations
  • Validates current user existence before proceeding
  • Follows proper async patterns with context.perform
  • Maps DTOs to models with appropriate error handling
  • Uses consistent completion callback patterns

The method integrates well with the live location sharing feature and provides a clean interface for fetching active live location messages for the current user.

Tests/StreamChatTests/Models/Attachments/StaticLocationAttachmentPayload_Tests.swift (1)

9-72: LGTM! Comprehensive test coverage for StaticLocationAttachmentPayload.

The test suite properly covers all key scenarios:

  • Decoding minimal JSON with required fields
  • Handling extra data fields for extensibility
  • Round-trip encoding/decoding validation

The test data uses realistic London coordinates and follows established patterns from the codebase.

Sources/StreamChat/Utils/Throttler.swift (3)

11-11: Good API design decision making the Throttler public.

Making this utility class public enables external usage for live location throttling, which aligns with the PR objectives.


18-18: Excellent change allowing runtime interval modification.

Changing interval from a constant to a public variable enables dynamic throttling adjustments, which could be valuable for adapting to different network conditions or user preferences in live location sharing.


28-28: Good improvement to queue initialization.

The explicit queue label and QoS setting improves debuggability and ensures appropriate priority for throttling operations.

Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents (2)

178-191: Well-designed Core Data entity for location data.

The LocationDTO entity design is solid:

  • Appropriate attribute types (Double for coordinates, optional Date for endAt)
  • Good uniqueness constraint on messageId prevents duplicate location data
  • Proper nullable relationships with MessageDTO
  • Supports both static and live location scenarios

265-265: Proper relationship design between MessageDTO and LocationDTO.

The optional to-one relationship with nullify deletion rule is the correct design choice, preventing cascade deletion issues while allowing messages without location data.

Tests/StreamChatTests/Models/Attachments/LiveLocationAttachmentPayload_Tests.swift (1)

58-76: LGTM! Comprehensive encoding test.

The encoding test properly validates round-trip serialization including the stoppedSharing field and extra data.

Sources/StreamChat/Models/Location/SharedLocation.swift (1)

7-48: LGTM! Well-designed location model.

The SharedLocation struct is well-designed with clear separation between static and live location concepts. The computed properties isLive and isLiveSharingActive provide intuitive ways to check location sharing status, and the implementation logic is correct.

DemoApp/Info.plist (1)

35-40: LGTM! Proper location permissions and background mode configuration.

The location permission descriptions are clear and user-friendly, and the background location mode is correctly configured for live location sharing functionality.

Also applies to: 62-65

DemoApp/StreamChat/Components/CustomAttachments/DemoQuotedChatMessageView.swift (1)

10-26: LGTM! Clean implementation using the new shared location model.

The updated implementation properly leverages the new sharedLocation property and provides clear visual differentiation between live and static locations using appropriate system icons and labels.

DemoApp/Screens/DemoAppTabBarController.swift (2)

11-11: LGTM! Proper LocationProvider integration.

The LocationProvider integration is well-implemented with proper weak self capture to prevent retain cycles, and the location transformation logic correctly converts CLLocation to LocationInfo.

Also applies to: 69-75


89-118: LGTM! Well-designed location sharing lifecycle management.

The delegate method implementations properly coordinate the location sharing lifecycle by starting/stopping location monitoring based on sharing status and providing useful debug logging for troubleshooting location updates.

Tests/StreamChatTests/Repositories/MessageRepository_Tests.swift (1)

726-763: Explicitly guarantee messages are considered “active”

The repository’s implementation is likely to filter out expired live-location messages (e.g. endAt <= Date()).
The dummy attachments created here do not specify an endAt, relying on implementation details that currently treat a nil value as “active forever”.

Providing an explicit future date clarifies intent and prevents flakiness if the definition of “active” changes:

-            attachments: [.dummy(type: .liveLocation)]
+            attachments: [.dummy(type: .liveLocation, endAt: Date().addingTimeInterval(3600))]

(Adjust helper signature if needed.)
This also makes the test name (returnsMessagesForChannel) fully reflect that the messages are indeed active.

TestTools/StreamChatTestTools/TestData/DummyData/MessageAttachmentPayload.swift (4)

2-2: Copyright year updated appropriately.

The copyright year has been updated to 2025, which is appropriate for new code additions.


177-188: Well-implemented static location factory method.

The static location factory method follows the established pattern with reasonable default coordinates (London) and proper payload structure. The latitude and longitude are correctly stored as numbers in the payload dictionary.


190-203: Live location factory method includes proper state management.

The live location factory method appropriately includes the stoppedSharing parameter, which is essential for managing live location sharing state. The default value of false makes sense for testing scenarios where location sharing is active.


205-213: Decoder properties follow established patterns.

Both decoder properties (decodedStaticLocationPayload and decodedLiveLocationPayload) follow the same pattern as existing decoders in the file, using proper error handling with optional return types and consistent encoding/decoding approach.

TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/MessageUpdater_Mock.swift (3)

117-123: Atomic properties properly defined for partial message updates.

The atomic properties correctly track all parameters of the updatePartialMessage method, including optional text, attachments, extraData, and completion handler. This follows the established pattern used throughout the mock class.


258-263: Cleanup method properly updated.

The cleanup method has been correctly updated to reset all the new atomic properties related to updatePartialMessage, ensuring proper test isolation between test runs.


533-547: Mock implementation follows established patterns.

The updatePartialMessage mock implementation correctly:

  • Stores all method parameters in atomic properties
  • Handles optional parameters appropriately
  • Invokes the completion result when available
  • Follows the same pattern as other mock methods in the class

This enables comprehensive testing of the partial message update functionality.

Sources/StreamChat/Workers/MessageUpdater.swift (2)

184-255: Well-designed partial message update implementation.

The updatePartialMessage method is correctly implemented with proper error handling and database synchronization. The attachment encoding logic properly converts AnyAttachmentPayload to MessageAttachmentPayload for API transmission.

Consider adding JSDoc-style documentation to describe the method's purpose and parameters, especially the unset parameter which may not be immediately clear to developers.


107-107: Consistent parameter addition for location support.

The addition of the optional location: NewLocationInfo? parameter and its corresponding nil defaults maintain backward compatibility while enabling location functionality.

Also applies to: 312-312

DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/UserAnnotationView.swift (1)

1-83: Well-implemented map annotation view with good separation of concerns.

The implementation follows iOS best practices:

  • Proper reuse identifier for map annotation views
  • Clean separation between avatar display and pulsing animation
  • Appropriate layer management and cleanup
Sources/StreamChat/Database/DatabaseSession.swift (3)

661-664: Well-designed protocol following existing patterns.

The LocationDatabaseSession protocol follows the same pattern as other database session protocols in the file, providing a clean separation of concerns for location-related database operations.


107-107: Backward-compatible parameter addition.

The optional location: NewLocationInfo? parameter maintains backward compatibility while enabling location functionality. The default nil value in the extension ensures existing code continues to work.

Also applies to: 305-305


661-680:

❌ Incorrect review comment

Ensure consistency with related location data structures.

The LocationPayload parameter type should be consistent with other location-related types in the codebase. Verify that this integrates properly with the NewLocationInfo type used in message creation.


🏁 Script executed:

#!/bin/bash
# Description: Verify consistency between LocationPayload and NewLocationInfo types
# Expected: Find type definitions and their relationships

rg "(LocationPayload|NewLocationInfo)" --type swift -A 3 -B 1

Length of output: 6621


🏁 Script executed:

#!/bin/bash
# Print LocationPayload definition
echo "---- LocationPayload ----"
sed -n '1,50p' Sources/StreamChat/APIClient/Endpoints/Payloads/LocationPayloads.swift
echo
# Print NewLocationInfo definition
echo "---- NewLocationInfo ----"
rg -n "struct NewLocationInfo" -A 5 Sources/StreamChat/Workers/ChannelUpdater.swift

Length of output: 1290


🏁 Script executed:

#!/bin/bash
# Print around struct MessageRequestBody or similar in MessagePayloads.swift
echo "---- MessageRequestPayload ----"
rg -n "struct.*Request" -n Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift -A5 -B2

Length of output: 446


``


🏁 Script executed:

#!/bin/bash
# Show the function where NewLocationInfo is used in ChannelUpdater
rg -n "location: NewLocationInfo" -n Sources/StreamChat/Workers/ChannelUpdater.swift -A5 -B5

Length of output: 536


🏁 Script executed:

#!/bin/bash
# Show saveMessage implementation to inspect location handling
rg -n "func saveMessage" -A30 -B5 Sources/StreamChat/Database/DTOs/MessageDTO.swift

Length of output: 7398


🏁 Script executed:

#!/bin/bash
# Show createNewMessage in ChannelUpdater to inspect location mapping
rg -n "func createNewMessage" -A20 -B10 Sources/StreamChat/Workers/ChannelUpdater.swift

Length of output: 2905


Ignore consistency suggestion for LocationPayload vs. NewLocationInfo

LocationPayload is the API-response model (including channelId, messageId and createdByDeviceId) used when reading/storing location attachments. NewLocationInfo is the minimal outgoing model (latitude, longitude, endAt) used when creating a new message. They serve different purposes and don’t need to match field-for-field.

Likely an incorrect or invalid review comment.

StreamChat.xcodeproj/project.pbxproj (33)

1431-1432: Add build entries for UserAnnotation components
The new PBXBuildFile entries for UserAnnotation.swift and UserAnnotationView.swift correctly include these source files in the build phase, aligning with the new map annotation features.


1469-1469: Include LocationSharingStatusView.swift in build phase
The LocationSharingStatusView.swift file has been added to the PBXBuildFile section, ensuring the status view is compiled.


1789-1790: Add test files for live and static location payloads
The new test targets LiveLocationAttachmentPayload_Tests.swift and StaticLocationAttachmentPayload_Tests.swift are included, improving coverage for the location attachment payloads.


1817-1818: Add LocationPayloads.swift to build phase
The LocationPayloads.swift source file has been correctly added to the build, providing the new payload definitions.


4232-4233: Add file references for UserAnnotation components
UserAnnotation.swift and UserAnnotationView.swift have been added to the project file references under the appropriate group.


4258-4258: Add file reference for LocationSharingStatusView.swift
The reference for LocationSharingStatusView.swift is included, ensuring the new view is discoverable by Xcode.


4326-4326: Add file reference for SharedLocation.swift
The SharedLocation.swift file is now referenced, enabling the shared location model in the project.


4398-4398: Add file reference for LocationDTO.swift
The LocationDTO.swift class is correctly referenced, supporting the Core Data mapping of location entities.


4474-4475: Add test references for location payloads
Test files LiveLocationAttachmentPayload_Tests.swift and StaticLocationAttachmentPayload_Tests.swift are now referenced in the project, aligning with the added test targets.


4497-4497: Add file reference for LocationPayloads.swift
The LocationPayloads.swift is now included in the file references, which provides the new location payload definitions.


4511-4513: Add file references for location core models
The files LocationInfo.swift, LocationProvider.swift, and Throttler.swift are correctly referenced, supporting live location updates and throttling.


5621-5621: Include LocationDTO.swift in Core Data model sources
The LocationDTO.swift class has been added to the managed object model targets, enabling persistence of location data.


5652-5652: Include Throttler.swift in utility sources
Throttler.swift is now part of the utilities source list, supporting rate-limiting for live location updates.


5714-5714: Include LocationProvider.swift in demo app sources
The LocationProvider.swift is added to the demo app’s sources, facilitating location feed in the demo.


5788-5788: Add LocationPayloads.swift to the Payloads group
LocationPayloads.swift is now part of the Payloads group, grouping it with other payload definitions.


5943-5943: Add Location group to Attachments
A new Location subgroup has been added under Attachments, organizing static and live location attachments.


7320-7321: Add test file references to Attachments group
The test files for live and static location attachments are now organized under the Attachments group, enhancing test discoverability.


8452-8454: Add UI components to LocationAttachment group
LocationSharingStatusView.swift, UserAnnotationView.swift, and UserAnnotation.swift are now grouped under LocationAttachment, matching the location attachment UI structure.


9132-9140: Add Location group with core files
The new Location group containing LocationInfo.swift and SharedLocation.swift was created, organizing core location models.


11126-11128: Add demo app UI for location components
Demo app sources LocationSharingStatusView.swift and UserAnnotation.swift are now included, showcasing the new live location UI.


11137-11138: Add demo app providers and views
LocationProvider.swift and UserAnnotationView.swift have been added to the demo app’s sources.


11422-11422: Include Throttler.swift in demo utilities
The Throttler.swift utility is added to the demo’s source list for live update throttling.


11463-11463: Add SharedLocation.swift to thread model sources
SharedLocation.swift is now included alongside thread-related DTOs, enabling message-level location sharing.


11615-11615: Include LocationDTO.swift in event handling sources
LocationDTO.swift is now part of the event middleware and observer logic.


11719-11719: Include LocationInfo.swift in channel model sources
The LocationInfo.swift model is now part of the channel-related source files.


11787-11787: Add LocationPayloads.swift to endpoint payload sources
LocationPayloads.swift is now referenced in the list of payload definitions for API endpoints.


11990-11991: Include location payload tests in core tests
The live/static location payload tests are now part of the core test suite, increasing coverage.


12288-12288: Add LocationDTO.swift to playback state sources
LocationDTO.swift is now included in the playback state modules for offline handling.


12375-12375: Include Throttler.swift in cache-and-debounce sources
Throttler.swift is added to the collection of debounce and caching utilities.


12551-12551: Include LocationInfo.swift in unread count sources
LocationInfo.swift is now part of the unread count-related logic, supporting contextual location badges.


12601-12601: Include SharedLocation.swift in SwiftUI components
SharedLocation.swift is now available in SwiftUI views for current user context.


12685-12685: Add LocationPayloads.swift to lazy map collection sources
The payload definitions for locations are now included in the lazy map collection utilities.


15458-15459: Bump Atlantis Swift package to 1.28.0
The remote Swift package reference for atlantis has been updated to use up-to-next-major 1.28.0, ensuring compatibility with the latest APIs.

DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentViewDelegate.swift (5)

9-17: Well-designed protocol separation.

The protocol effectively separates location viewing from stop sharing actions, providing clear responsibilities for each delegate method. The migration to SharedLocation aligns with the broader refactoring mentioned in the summary.


28-44: Appropriate platform-specific presentation logic.

The implementation correctly handles different device types with modal presentation on iPad and push navigation on other devices. The stop sharing functionality properly uses the channel controller to manage live location sharing state.


7-7: UIKit import addition looks good.

The UIKit import is appropriately added since the code uses UIDevice and UINavigationController types.


10-16: Protocol refactoring improves separation of concerns.

The migration from a single didTapOnLocationAttachment(_:) method to two distinct methods (didTapOnLocation(_:) and didTapOnStopSharingLocation(_:)) provides better separation of concerns and clearer intent for different user actions.


20-26:

✅ Verification successful

Verify message controller creation and lifecycle management.

The implementation creates a new ChatMessageController for each location tap. Consider the following:

  1. Memory management: Ensure the controller is properly retained while the detail view is presented
  2. Error handling: The controller creation could potentially fail or return an invalid controller

Let me verify how ChatMessageController creation is handled:


🏁 Script executed:

#!/bin/bash
# Description: Check how ChatMessageController is created and if error handling is needed
# Expected: Find the messageController method implementation and error handling patterns

ast-grep --pattern 'func messageController($$$) -> $_' 

# Also check for similar usage patterns in the codebase
rg -A 5 "client\.messageController"

Length of output: 22137


🏁 Script executed:

#!/bin/bash
# Display the LocationAttachmentViewDelegate to inspect showDetailViewController usage
sed -n '1,200p' DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentViewDelegate.swift

Length of output: 1522


No action needed – ChatMessageController setup is safe

The messageController(cid:messageId:) initializer always returns a non‐optional ChatMessageController, and handing it off to LocationDetailViewController (which stores it as a property) ensures it’s retained for the lifetime of the detail view. You can remove the earlier concerns around memory management and error handling.

Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift (12)

57-57: Correct coding key mapping for location field.

The coding key properly maps the location property to the backend shared_location field, maintaining consistency with the API contract.


116-116: Appropriate optional location property.

The location property is correctly declared as optional since location data is not present in all messages. The mutable var declaration allows for potential updates to location information.


183-183: Proper optional decoding implementation.

The location decoding correctly uses decodeIfPresent and follows the established pattern for optional fields in the payload structure.


289-289: Complete integration of location support in outgoing messages.

The location field is properly integrated into the outgoing message request body with:

  • Optional property declaration
  • Default parameter in initializer
  • Proper assignment in constructor
  • Conditional encoding logic

The use of LocationRequestPayload type for outgoing data suggests good separation of concerns between incoming and outgoing payload structures.

Also applies to: 310-310, 329-329, 348-348


57-57: Location coding key correctly maps to backend field.

The mapping of location = "shared_location" aligns with the backend API specification mentioned in the PR objectives.


116-116: Location property addition is consistent with other optional fields.

The location: LocationPayload? property follows the same pattern as other optional fields like poll and draft.


183-183: Location decoding implementation is correct.

The decoding logic location = try container.decodeIfPresent(LocationPayload.self, forKey: .location) properly handles optional location data and follows the same pattern as other optional fields.


265-265: Location assignment in initializer is correct.

The assignment self.location = location is consistent with other properties and properly initializes the location field.


289-289: MessageRequestBody location property follows correct pattern.

The addition of var location: LocationRequestPayload? to the outgoing message body is consistent with the incoming payload structure.


310-310: MessageRequestBody initializer parameter addition is correct.

The location parameter with default nil value maintains backward compatibility while enabling location support.


329-329: Location assignment in MessageRequestBody initializer is correct.

The assignment follows the same pattern as other properties in the initializer.


348-348: Location encoding implementation is correct.

The encoding logic try container.encodeIfPresent(location, forKey: .location) properly handles optional location data and follows the established pattern for optional fields.

CHANGELOG.md (1)

7-22: Changelog entries accurately capture new location features and formatting is correct
The “Upcoming” section lists all additions from PR #3531—partial message updates, static location support, and live location sharing APIs—and follows the existing indentation and Keep a Changelog style.

Sources/StreamChat/Database/DTOs/LocationDTO.swift (1)

18-35: deviceId is non-optional but source may be empty

If payload.createdByDeviceId can legitimately be nil (e.g. Web-socket events coming from another user without device info) the assignment to a non-optional String will crash.

Either:

  1. Mark deviceId optional in the data model, or
  2. Provide a safe fallback:
- locationDTO.deviceId = payload.createdByDeviceId
+ locationDTO.deviceId = payload.createdByDeviceId ?? ""

Validate upstream assumptions to avoid runtime failures.

Sources/StreamChat/Controllers/MessageController/MessageController.swift (2)

365-374: Passing an empty attachment array may wipe the attachment on the backend

updatePartialMessage is called with attachments: [].
Many REST back-ends treat an empty array as “replace current value with empty array”, effectively removing the live-location attachment. In this flow we only want to update coordinates, not strip the attachment.

Prefer sending attachments: nil (do not touch) or use the unset parameter explicitly when removal is intended.

-            attachments: [],
+            attachments: nil,

Please align the semantics here with the server implementation.


1197-1208: 👍 Clear, domain-specific error types added

The new ClientError subclasses precisely describe the two failure scenarios around live-location handling. This increases debuggability and allows callers to react programmatically. Looks good.

DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationDetailViewController.swift (1)

42-46: No error handling when stopping live sharing
messageController.stopLiveLocationSharing() is fire-and-forget. If the operation fails (e.g. network error) the UI remains in the “stopping” state silently. Provide a completion handler and surface the error (alert, toast, etc.) so users understand what happened.

Sources/StreamChat/Database/DTOs/MessageDTO.swift (1)

1638-1645: Verify REST field name for device identifier

asRequestBody() serialises the live-location with createdByDeviceId.
Please double-check the backend contract – earlier payloads/PR description showed the field as deviceId. A mismatch will silently drop the value server-side.

Comment on lines +23 to +24
<key>LSApplicationCategoryType</key>
<string></string>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove or set proper value for LSApplicationCategoryType.

The LSApplicationCategoryType key is set to an empty string, which is not a valid App Store category. Either remove this key entirely or set it to a proper category like "public.app-category.social-networking".

-	<key>LSApplicationCategoryType</key>
-	<string></string>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<key>LSApplicationCategoryType</key>
<string></string>
🤖 Prompt for AI Agents
In DemoApp/Info.plist at lines 23 to 24, the LSApplicationCategoryType key is
set to an empty string, which is invalid. Fix this by either removing the
LSApplicationCategoryType key entirely or setting its value to a valid App Store
category string such as "public.app-category.social-networking".

Comment on lines 7 to 23
struct LocationPayload: Decodable {
let channelId: String
let messageId: String
let latitude: Double
let longitude: Double
let endAt: Date?
let createdByDeviceId: String

enum CodingKeys: String, CodingKey {
case channelId = "channel_cid"
case messageId = "message_id"
case latitude
case longitude
case createdByDeviceId = "created_by_device_id"
case endAt = "end_at"
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Clarify visibility & optionality of payload fields

  1. Access-level
    Other payload structs inside StreamChat are public because apps need to construct them for requests. Consider matching that for consistency:
-struct LocationPayload: Decodable {
+public struct LocationPayload: Decodable {

Do the same for LocationRequestPayload if it’s part of the public surface.

  1. Device identifier
    Servers might omit created_by_device_id when the value is unknown, so marking it optional prevents decoding failures:
-    let createdByDeviceId: String
+    let createdByDeviceId: String?

(Repeat in the request struct if callers may leave it out.)

These tweaks improve API robustness while avoiding breaking changes later.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
struct LocationPayload: Decodable {
let channelId: String
let messageId: String
let latitude: Double
let longitude: Double
let endAt: Date?
let createdByDeviceId: String
enum CodingKeys: String, CodingKey {
case channelId = "channel_cid"
case messageId = "message_id"
case latitude
case longitude
case createdByDeviceId = "created_by_device_id"
case endAt = "end_at"
}
}
public struct LocationPayload: Decodable {
let channelId: String
let messageId: String
let latitude: Double
let longitude: Double
let endAt: Date?
let createdByDeviceId: String?
enum CodingKeys: String, CodingKey {
case channelId = "channel_cid"
case messageId = "message_id"
case latitude
case longitude
case createdByDeviceId = "created_by_device_id"
case endAt = "end_at"
}
}
🤖 Prompt for AI Agents
In Sources/StreamChat/APIClient/Endpoints/Payloads/LocationPayloads.swift around
lines 7 to 23, update the LocationPayload struct to be public for consistency
with other payloads used in requests. Also, change the createdByDeviceId
property to be optional to handle cases where the server omits this field,
preventing decoding errors. Apply the same visibility and optionality changes to
LocationRequestPayload if it is part of the public API surface.

Comment on lines +221 to +236
self?.database.write { session in
let cid: ChannelId?

if let payloadCid = messagePayloadBoxed.message.cid {
cid = payloadCid
} else if let cidFromLocal = session.message(id: messageId)?.cid,
let localCid = try? ChannelId(cid: cidFromLocal) {
cid = localCid
} else {
cid = nil
}

guard let cid = cid else {
completion?(.failure(ClientError.ChannelNotCreatedYet()))
return
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

Verify channel ID resolution logic for edge cases.

The channel ID resolution logic handles both payload-provided and locally-stored channel IDs appropriately. However, ensure this logic works correctly when messages are created in channels that exist locally but haven't been fully synchronized.


🏁 Script executed:

#!/bin/bash
# Description: Verify that partial message updates handle channel ID resolution correctly
# Expected: Find tests that validate channel ID resolution in partial updates

rg -A 10 "updatePartialMessage.*test" --type swift

Length of output: 52


Add tests for channel ID resolution in partial message updates

I wasn’t able to find any existing tests covering the CID‐fallback logic in MessageUpdater.swift (Sources/StreamChat/Workers/MessageUpdater.swift lines 221–236). Please add unit tests for these scenarios to ensure edge cases are handled:

• Payload provides a valid cid → update uses the payload CID
• Payload’s cid is nil but local DB session has a message with a valid CID → update falls back to local CID
• Both payload and local session CID are missing → update fails with ClientError.ChannelNotCreatedYet()

🤖 Prompt for AI Agents
In Sources/StreamChat/Workers/MessageUpdater.swift around lines 221 to 236, add
unit tests to cover the channel ID resolution logic in partial message updates.
Create tests for these cases: when the payload provides a valid cid, when the
payload cid is nil but the local database session has a valid cid for the
message, and when both payload and local session cids are missing, ensuring the
update fails with ClientError.ChannelNotCreatedYet(). These tests should verify
that the fallback logic behaves correctly in each scenario.

Comment on lines +679 to +680
PollDatabaseSession,
LocationDatabaseSession {}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify implementation of LocationDatabaseSession protocol.

The protocol is properly added to the main DatabaseSession inheritance list, but ensure that there's a concrete implementation of the saveLocation method somewhere in the codebase.


🏁 Script executed:

#!/bin/bash
# Description: Find implementation of LocationDatabaseSession protocol methods
# Expected: Find concrete implementations of saveLocation method

rg "func saveLocation" --type swift -A 5

Length of output: 1107


🏁 Script executed:

#!/bin/bash
# Show LocationDatabaseSession protocol definition
rg -n "protocol LocationDatabaseSession" -A5 -B5 Sources/StreamChat/Database/DatabaseSession.swift

# Show any default implementation via protocol extension
rg -n "extension LocationDatabaseSession" -A5 -B5 Sources/StreamChat/Database

# List types conforming to DatabaseSession
rg -n "class .*:.*DatabaseSession" -A5 -B5 --type swift Sources/StreamChat
rg -n "struct .*:.*DatabaseSession" -A5 -B5 --type swift Sources/StreamChat

Length of output: 770


Missing implementation for LocationDatabaseSession.saveLocation

I couldn’t find any concrete saveLocation(payload:cache:) implementation in your codebase:

  • The LocationDatabaseSession protocol is declared in Sources/StreamChat/Database/DatabaseSession.swift, but there’s no default implementation in a protocol extension.
  • No class or struct conforming to DatabaseSession implements saveLocation.

Please add a saveLocation(payload: LocationPayload, cache: PreWarmedCache?) throws -> LocationDTO implementation—either in your DatabaseSession conformer (e.g. your CoreData/SQLite session class) or via a protocol extension, using LocationDTO.loadOrCreate(...) under the hood.

🤖 Prompt for AI Agents
In Sources/StreamChat/Database/DatabaseSession.swift around lines 679 to 680,
the LocationDatabaseSession protocol is added to DatabaseSession but lacks a
concrete implementation of the required saveLocation(payload:cache:) method. To
fix this, implement the saveLocation method either in the class or struct
conforming to DatabaseSession (such as your CoreData or SQLite session class) or
provide a default implementation in a protocol extension. The implementation
should use LocationDTO.loadOrCreate(...) internally to save the location data
and return the LocationDTO instance.

Comment on lines 988 to 1008
switch result {
case let .success(messages):
guard let message = messages.first,
let location = message.sharedLocation
else {
self.callback {
completion?(.failure(ClientError.MessageDoesNotHaveLiveLocationAttachment()))
}
return
}

/// Stop sharing the live location message.
self.messageUpdater.updatePartialMessage(
messageId: message.id,
attachments: []
) { result in
self.callback {
completion?(result.map(\.id))
}
}
case let .failure(error):
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Unused location constant and no end-time set when stopping sharing

  1. let location = message.sharedLocation is never used after the guard, producing a compiler warning.
  2. updatePartialMessage(attachments: []) removes attachments but doesn’t mark the live-location as ended (e.g. by setting endAt = Date()). Clients may keep considering the share “active”.
-                guard let message = messages.first,
-                      let location = message.sharedLocation
+                guard let message = messages.first,
+                      message.sharedLocation != nil

and, if the backend requires an end_at, pass it explicitly:

self.messageUpdater.updatePartialMessage(
    messageId: message.id,
    locationEndAt: Date()
) {  }

(Exact API may differ.)


🏁 Script executed:

#!/bin/bash
# Locate the definition(s) of updatePartialMessage to inspect its parameters
rg "func updatePartialMessage" -n .

# Find all occurrences of sharedLocation on Message to see its type and usage
rg "sharedLocation" -n .

Length of output: 3601


🏁 Script executed:

#!/bin/bash
# Display the signature of updatePartialMessage to see available parameters
rg -A 5 -n "func updatePartialMessage" -n Sources/StreamChat/Workers/MessageUpdater.swift

Length of output: 334



---

🏁 Script executed:

```shell
#!/bin/bash
# Find the definition of SharedLocation and inspect its properties
rg -n "struct SharedLocation" -A 15 -n Sources/StreamChat/Models/ChatMessage.swift

# Check if updatePartialMessage extraData parameter is used elsewhere for location end
rg "updatePartialMessage.*extraData" -n .

Length of output: 726


🏁 Script executed:

#!/bin/bash
# Search entire repository for SharedLocation definition
rg -n "struct SharedLocation" -n .

# If not found, search for SharedLocation as a typealias or class
rg -n "class SharedLocation" -n .

# Search for endAt property anywhere
rg -n "endAt" -n .

Length of output: 3317


Remove unused binding and mark live location as ended

The location constant is never used and you also need to tell the backend to end the live‐location share (simply clearing attachments won’t set endAt).

• Update the guard to drop the unused binding:

-                guard let message = messages.first,
-                      let location = message.sharedLocation
+                guard let message = messages.first,
+                      message.sharedLocation != nil

• When stopping the share, clear the attachments and include an end_at timestamp. You can pass it via extraData or extend updatePartialMessage with a locationEndAt parameter. For example:

let endDate = Date()
self.messageUpdater.updatePartialMessage(
    messageId: message.id,
    attachments: [],
    extraData: ["location": [
        "end_at": endDate
    ]]
) { result in
    self.callback {
        completion?(result.map(\.id))
    }
}

Adjust the key/value structure to match your backend schema, or add a dedicated locationEndAt: Date? parameter to updatePartialMessage if that’s preferred.

🤖 Prompt for AI Agents
In Sources/StreamChat/Controllers/ChannelController/ChannelController.swift
around lines 988 to 1008, remove the unused 'location' binding from the guard
statement and modify the call to updatePartialMessage to not only clear
attachments but also mark the live location share as ended by passing an
'end_at' timestamp. This can be done by adding an extraData dictionary with the
appropriate key/value (e.g., ["location": ["end_at": Date()]]) or by using a
locationEndAt parameter if available, ensuring the backend is informed that the
live location sharing has stopped.

Comment on lines 626 to 644
) -> NSFetchRequest<MessageDTO> {
let request = NSFetchRequest<MessageDTO>(entityName: MessageDTO.entityName)
MessageDTO.applyPrefetchingState(to: request)
// Hard coded limit for now. 10 live locations messages at the same should be more than enough.
request.fetchLimit = 10
request.sortDescriptors = [NSSortDescriptor(
keyPath: \MessageDTO.createdAt,
ascending: true
)]
var predicates: [NSPredicate] = [
.init(format: "location.endAt < %@", Date().bridgeDate),
.init(format: "user.id == %@", currentUserId)
]
if let channelId {
predicates.append(.init(format: "channel.cid == %@", channelId.rawValue))
}
request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
return request
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Predicate selects expired instead of active live-location messages

activeLiveLocationMessagesFetchRequest intends to return currently running live-location messages, however the predicate

.init(format: "location.endAt < %@", Date().bridgeDate)

matches rows whose endAt is earlier than “now”, i.e. already expired.
It also ignores messages with an open-ended share (endAt == nil) and messages without a location object.

-            .init(format: "location.endAt < %@", Date().bridgeDate),
+            // Active = (no end date OR end date is in the future)
+            NSPredicate(
+                format: "location != nil AND (location.endAt == nil OR location.endAt > %@)",
+                Date().bridgeDate
+            ),

While touching the predicate, consider lifting the magic fetchLimit = 10 into a named constant or a parameter to avoid accidental hard-caps.

🤖 Prompt for AI Agents
In Sources/StreamChat/Database/DTOs/MessageDTO.swift around lines 626 to 644,
the predicate incorrectly filters for expired live-location messages by checking
if location.endAt is less than the current date, excluding active messages and
those with nil endAt or missing location. Update the predicate to select
messages where location.endAt is either nil or greater than the current date to
capture active live-location messages, and also handle cases where location may
be nil. Additionally, replace the hardcoded fetchLimit of 10 with a named
constant or a function parameter to make the limit configurable and avoid
accidental hard caps.

Comment on lines 224 to 225
draft: DraftPayload? = nil,
location: LocationPayload? = nil
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify LocationPayload and LocationRequestPayload types exist.

The init method parameter addition looks correct, but we should verify that the LocationPayload type is properly defined and imported.


🏁 Script executed:

#!/bin/bash
# Description: Verify LocationPayload and LocationRequestPayload types are defined
# Expected: Find the definitions of these types

ast-grep --pattern 'struct LocationPayload'
ast-grep --pattern 'struct LocationRequestPayload'

# Also check if they are imported or defined in this module
rg "LocationPayload" --type swift -A 3

Length of output: 3672


Ensure LocationPayload Is Defined and Clarify LocationRequestPayload

  • ✅ LocationPayload is defined in Sources/StreamChat/APIClient/Endpoints/Payloads/LocationPayloads.swift
  • ❌ No type named LocationRequestPayload was found in the codebase; if you intended to introduce a dedicated request payload type, please add or import it, or remove this reference from the review.
🤖 Prompt for AI Agents
In Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift around
lines 224 to 225, verify that LocationPayload is correctly imported from
LocationPayloads.swift since it is used as a parameter type. Also, since
LocationRequestPayload does not exist in the codebase, remove any references to
LocationRequestPayload or create and import this type if it is intended to be
used.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
DemoApp/StreamChat/Components/CustomAttachments/DemoComposerVC.swift (1)

64-80: ⚠️ Potential issue

Critical: Thread safety issue remains unresolved.

The completion handler from locationProvider.getCurrentLocation may execute on a background thread, but the code performs UI updates and SDK controller calls without ensuring main thread execution.

As noted in the previous review, wrap the completion block in DispatchQueue.main.async:

 private func getCurrentLocationInfo(completion: @escaping (LocationInfo?) -> Void) {
     locationProvider.getCurrentLocation { [weak self] result in
-        switch result {
-        case .success(let location):
-            let location = LocationInfo(
-                latitude: location.coordinate.latitude,
-                longitude: location.coordinate.longitude
-            )
-            completion(location)
-        case .failure(let error):
-            if error is LocationPermissionError {
-                self?.showLocationPermissionAlert()
-            }
-            completion(nil)
-        }
+        DispatchQueue.main.async {
+            switch result {
+            case .success(let location):
+                let location = LocationInfo(
+                    latitude: location.coordinate.latitude,
+                    longitude: location.coordinate.longitude
+                )
+                completion(location)
+            case .failure(let error):
+                if error is LocationPermissionError {
+                    self?.showLocationPermissionAlert()
+                }
+                completion(nil)
+            }
+        }
     }
 }
Sources/StreamChat/APIClient/Endpoints/Payloads/LocationPayloads.swift (1)

7-23: 🛠️ Refactor suggestion

Address previous review feedback on visibility and optionality.

The previous review comment about making these structs public and createdByDeviceId optional is still relevant and should be addressed.

Apply the suggested changes for API robustness:

-struct SharedLocationPayload: Decodable {
+public struct SharedLocationPayload: Decodable {
     let channelId: String
     let messageId: String
     let latitude: Double
     let longitude: Double
     let endAt: Date?
-    let createdByDeviceId: String
+    let createdByDeviceId: String?

This prevents decoding failures when the server omits the device ID and ensures consistency with other public payload structs in the SDK.

🧹 Nitpick comments (1)
DemoApp/StreamChat/Components/CustomAttachments/DemoComposerVC.swift (1)

49-49: Consider making live location duration configurable.

The 2-hour duration is hardcoded, which limits flexibility for different use cases. Consider making this configurable through app settings or user selection.

You could add a configuration property or present duration options to the user:

// Option 1: Configuration property
private let defaultLiveLocationDuration: TimeInterval = AppConfig.shared.defaultLiveLocationDuration

// Option 2: User selection
private func showLiveLocationDurationPicker(completion: @escaping (TimeInterval) -> Void) {
    // Present action sheet with duration options
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ce36472 and 9311f09.

📒 Files selected for processing (16)
  • DemoApp/StreamChat/Components/CustomAttachments/DemoComposerVC.swift (1 hunks)
  • DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentViewDelegate.swift (1 hunks)
  • Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift (1 hunks)
  • Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift (2 hunks)
  • Sources/StreamChat/APIClient/Endpoints/LocationEndpoints.swift (1 hunks)
  • Sources/StreamChat/APIClient/Endpoints/Payloads/LocationPayloads.swift (1 hunks)
  • Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift (9 hunks)
  • Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (7 hunks)
  • Sources/StreamChat/Controllers/MessageController/MessageController.swift (7 hunks)
  • Sources/StreamChat/Database/DTOs/LocationDTO.swift (1 hunks)
  • Sources/StreamChat/Database/DTOs/MessageDTO.swift (10 hunks)
  • Sources/StreamChat/Database/DatabaseSession.swift (4 hunks)
  • Sources/StreamChat/Models/ChatMessage.swift (6 hunks)
  • Sources/StreamChat/Models/Location/SharedLocation.swift (1 hunks)
  • Sources/StreamChat/Workers/MessageUpdater.swift (28 hunks)
  • StreamChat.xcodeproj/project.pbxproj (39 hunks)
✅ Files skipped from review due to trivial changes (2)
  • Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift
  • Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift
🚧 Files skipped from review as they are similar to previous changes (11)
  • Sources/StreamChat/Models/Location/SharedLocation.swift
  • Sources/StreamChat/Models/ChatMessage.swift
  • StreamChat.xcodeproj/project.pbxproj
  • Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift
  • DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentViewDelegate.swift
  • Sources/StreamChat/Controllers/ChannelController/ChannelController.swift
  • Sources/StreamChat/Database/DatabaseSession.swift
  • Sources/StreamChat/Database/DTOs/LocationDTO.swift
  • Sources/StreamChat/Workers/MessageUpdater.swift
  • Sources/StreamChat/Controllers/MessageController/MessageController.swift
  • Sources/StreamChat/Database/DTOs/MessageDTO.swift
🧰 Additional context used
🧬 Code Graph Analysis (1)
Sources/StreamChat/APIClient/Endpoints/LocationEndpoints.swift (1)
TestTools/StreamChatTestTools/TestData/DummyData/MessageAttachmentPayload.swift (1)
  • liveLocation (190-203)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Test LLC (Debug)
  • GitHub Check: Metrics
  • GitHub Check: Build Test App and Frameworks
🔇 Additional comments (2)
Sources/StreamChat/APIClient/Endpoints/LocationEndpoints.swift (1)

7-43: Well-structured endpoint definitions with clear separation of concerns.

The endpoint implementations follow the SDK's established patterns effectively:

  • Proper HTTP method usage (PUT for mutations, GET for queries)
  • Consistent authentication requirements
  • Type-safe return types with appropriate payload generics
  • Clear documentation explaining the relationship to message creation

The shared .liveLocation path with different HTTP methods is a clean RESTful approach for managing the same resource lifecycle.

DemoApp/StreamChat/Components/CustomAttachments/DemoComposerVC.swift (1)

82-97: Good implementation of location permission handling.

The permission alert provides clear guidance to users and offers a direct path to Settings. The implementation handles the permission error case appropriately.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
Sources/StreamChat/Workers/CurrentUserUpdater.swift (2)

232-248: Add documentation comments for consistency.

The method lacks documentation comments while other methods in the class have comprehensive documentation. Please add documentation following the existing pattern to describe the method's purpose, parameters, and completion behavior.

+    /// Loads the current user's active live location messages from the backend.
+    /// 
+    /// - Parameter completion: Called when the request is finished. Returns an array of `SharedLocation` objects on success, or an error on failure.
     func loadActiveLiveLocations(completion: @escaping (Result<[SharedLocation], Error>) -> Void) {

232-248: Add async version for API consistency.

Most other methods in this class have corresponding async versions in the extension (lines 251-330). Consider adding an async version of loadActiveLiveLocations to maintain consistency with the existing API design.

Add this method to the async extension:

     func updateUserData(
         currentUserId: UserId,
         name: String?,
         imageURL: URL?,
         privacySettings: UserPrivacySettings?,
         role: UserRole?,
         teamsRole: [TeamId: UserRole]?,
         userExtraData: [String: RawJSON]?,
         unset: Set<String>
     ) async throws {
         try await withCheckedThrowingContinuation { continuation in
             updateUserData(
                 currentUserId: currentUserId,
                 name: name,
                 imageURL: imageURL,
                 privacySettings: privacySettings,
                 role: role,
                 teamsRole: teamsRole,
                 userExtraData: userExtraData,
                 unset: unset
             ) { error in
                 continuation.resume(with: error)
             }
         }
     }
+    
+    func loadActiveLiveLocations() async throws -> [SharedLocation] {
+        try await withCheckedThrowingContinuation { continuation in
+            loadActiveLiveLocations { result in
+                continuation.resume(with: result)
+            }
+        }
+    }
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9311f09 and 6c1f1b0.

📒 Files selected for processing (8)
  • DemoApp/Screens/DemoAppTabBarController.swift (4 hunks)
  • Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift (1 hunks)
  • Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift (2 hunks)
  • Sources/StreamChat/APIClient/Endpoints/LocationEndpoints.swift (1 hunks)
  • Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift (8 hunks)
  • Sources/StreamChat/Controllers/MessageController/MessageController.swift (7 hunks)
  • Sources/StreamChat/Workers/CurrentUserUpdater.swift (1 hunks)
  • Sources/StreamChat/Workers/MessageUpdater.swift (28 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift
  • Sources/StreamChat/APIClient/Endpoints/EndpointPath+OfflineRequest.swift
  • DemoApp/Screens/DemoAppTabBarController.swift
  • Sources/StreamChat/Controllers/MessageController/MessageController.swift
  • Sources/StreamChat/Workers/MessageUpdater.swift
  • Sources/StreamChat/Controllers/CurrentUserController/CurrentUserController.swift
  • Sources/StreamChat/APIClient/Endpoints/LocationEndpoints.swift
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: Test LLC (Debug)
  • GitHub Check: Build LLC + UI (Xcode 15)
  • GitHub Check: Automated Code Review
  • GitHub Check: Build Test App and Frameworks
  • GitHub Check: Metrics

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c1f1b0 and 9a4d314.

📒 Files selected for processing (14)
  • StreamChat.xcodeproj/project.pbxproj (35 hunks)
  • TestTools/StreamChatTestTools/Extensions/Unique/ChatMessage+Unique.swift (1 hunks)
  • TestTools/StreamChatTestTools/Mocks/Models + Extensions/ChatMessage_Mock.swift (2 hunks)
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift (4 hunks)
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift (3 hunks)
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/MessageUpdater_Mock.swift (5 hunks)
  • TestTools/StreamChatTestTools/SpyPattern/Spy/DatabaseContainer_Spy.swift (2 hunks)
  • TestTools/StreamChatTestTools/TestData/DummyData/MessageAttachmentPayload.swift (1 hunks)
  • TestTools/StreamChatTestTools/TestData/DummyData/MessagePayload.swift (2 hunks)
  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift (5 hunks)
  • Tests/StreamChatTests/Controllers/MessageController/MessageController_Tests.swift (1 hunks)
  • Tests/StreamChatTests/Database/DTOs/MessageDTO_Tests.swift (13 hunks)
  • Tests/StreamChatTests/Models/ChatMessage_Tests.swift (1 hunks)
  • Tests/StreamChatTests/Repositories/MessageRepository_Tests.swift (1 hunks)
✅ Files skipped from review due to trivial changes (4)
  • TestTools/StreamChatTestTools/TestData/DummyData/MessageAttachmentPayload.swift
  • TestTools/StreamChatTestTools/Extensions/Unique/ChatMessage+Unique.swift
  • Tests/StreamChatTests/Models/ChatMessage_Tests.swift
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift
🚧 Files skipped from review as they are similar to previous changes (4)
  • StreamChat.xcodeproj/project.pbxproj
  • Tests/StreamChatTests/Repositories/MessageRepository_Tests.swift
  • Tests/StreamChatTests/Database/DTOs/MessageDTO_Tests.swift
  • Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: Build LLC + UI (Xcode 15)
  • GitHub Check: Test LLC (Debug)
  • GitHub Check: Automated Code Review
  • GitHub Check: Build Test App and Frameworks
  • GitHub Check: Metrics
🔇 Additional comments (10)
TestTools/StreamChatTestTools/Mocks/Models + Extensions/ChatMessage_Mock.swift (1)

53-54: LGTM! Clean addition of location sharing support to test mocks.

The sharedLocation parameter addition follows the established pattern of other optional parameters in the mock factory, with appropriate type and default value.

Also applies to: 95-96

TestTools/StreamChatTestTools/TestData/DummyData/MessagePayload.swift (1)

54-55: LGTM! Proper addition of location sharing support to dummy data factory.

The parameter mapping from sharedLocation to location is correct and aligns with the MessagePayload initializer signature. The type and default value are appropriate for payload-level testing.

Also applies to: 97-98

TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift (2)

143-143: LGTM! Correct addition of location parameter to message creation.

The location parameter is properly typed as NewLocationInfo? for the database layer and correctly forwarded to the underlying session.

Also applies to: 167-167


267-269: LGTM! Proper implementation of location saving delegation.

The saveLocation method follows the established delegation pattern used throughout the mock class, correctly forwarding to the underlying session.

Tests/StreamChatTests/Controllers/MessageController/MessageController_Tests.swift (3)

2576-2583: LGTM - Well-structured test helper method.

The helper method properly handles async delegate callback testing with XCTestExpectation and follows standard XCTest patterns.


2585-2647: Comprehensive test coverage for partial message updates.

The test methods properly cover the new partialUpdateMessage functionality with appropriate parameter validation, error propagation, and success handling. The async testing patterns and mocking approach are consistent with the existing codebase.


2649-2829: Excellent test coverage for live location functionality.

The live location tests comprehensively cover both success and error scenarios for updateLiveLocation and stopLiveLocationSharing methods. The tests properly validate:

  • Correct parameter passing to the message updater
  • Error handling for edge cases (already stopped, no attachment)
  • Proper async behavior with expectations
  • Correct error types returned

The mocking strategy using SharedLocation objects and message observer is appropriate and follows existing patterns in the codebase.

TestTools/StreamChatTestTools/SpyPattern/Spy/DatabaseContainer_Spy.swift (1)

294-294: LGTM! Clean addition of location support to test infrastructure.

The new location parameter is properly integrated into the message creation flow with appropriate default value and consistent naming.

Also applies to: 324-324

TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/MessageUpdater_Mock.swift (2)

33-38: LGTM! Well-structured mock properties for new location features.

The atomic properties follow the established pattern and properly capture all necessary parameters for testing the location-related methods.

Also applies to: 125-130


339-355: LGTM! Mock implementations correctly follow established patterns.

The new mock methods properly capture parameters and support completion handler testing, maintaining consistency with existing mock implementations.

Also applies to: 564-578

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentViewInjector.swift (1)

18-18: Apply width constraint to the correct view

The width constraint is still being applied to bubbleThreadFootnoteContainer instead of locationAttachmentView, which means the footnote container gets stretched while the map keeps its intrinsic width.

-        contentView.bubbleThreadFootnoteContainer.width(mapWidth)
+        locationAttachmentView.width(mapWidth)
🧹 Nitpick comments (1)
DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationDetailViewController.swift (1)

134-149: Consider making animation duration configurable

The 5-second animation duration is hardcoded and might not feel natural for all use cases. The comment explains the rationale, but this could be made configurable or calculated based on update frequency.

+private let locationUpdateAnimationDuration: TimeInterval = 5.0
+private let mapCenterAnimationDuration: TimeInterval = 5.2

-                UIView.animate(withDuration: 5, delay: 0, options: .allowUserInteraction) {
+                UIView.animate(withDuration: locationUpdateAnimationDuration, delay: 0, options: .allowUserInteraction) {
                    existingAnnotation.coordinate = coordinate
                }
                if isAutoCenteringEnabled {
-                    UIView.animate(withDuration: 5, delay: 0.2, options: [.curveEaseOut, .allowUserInteraction]) {
+                    UIView.animate(withDuration: mapCenterAnimationDuration, delay: 0.2, options: [.curveEaseOut, .allowUserInteraction]) {
                        self.mapView.setCenter(coordinate, animated: true)
                    }
                }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a4d314 and 62fe047.

📒 Files selected for processing (3)
  • DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentViewInjector.swift (1 hunks)
  • DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationDetailViewController.swift (2 hunks)
  • Sources/StreamChat/Workers/MessageUpdater.swift (28 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/StreamChat/Workers/MessageUpdater.swift
🔇 Additional comments (4)
DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationAttachmentViewInjector.swift (2)

20-26: Good separation of tap handlers

The implementation correctly separates location tap handling from stop sharing action, addressing the previous concern about conflating these two distinct user actions.


39-47:

✅ Verification successful

Verify shared location property availability

The code accesses contentView.content?.sharedLocation but ensure this property is consistently available across all message types that use this injector.


🏁 Script executed:

#!/bin/bash
# Description: Verify that sharedLocation property exists and is used consistently
# Expected: Find the property definition and its usage patterns

# Search for sharedLocation property definition
ast-grep --pattern 'var sharedLocation: $_'

# Search for sharedLocation usage in message content
rg -A 3 -B 3 'sharedLocation'

Length of output: 35406


No action needed: sharedLocation is always available
The sharedLocation property is declared on ChatMessage (Sources/StreamChat/Models/ChatMessage.swift) as an optional and is consistently populated from payloads (MessagePayload.sharedLocation). The injector already safely unwraps it before use—no further changes are required.

DemoApp/StreamChat/Components/CustomAttachments/LocationAttachment/LocationDetailViewController.swift (2)

201-227: Good implementation of live location updates

The message controller delegate implementation correctly handles live location updates, including stopping animations when sharing ends and updating the banner state.


57-59:

✅ Verification successful

Verify memory management in closure

The closure captures self weakly, which is correct, but ensure that messageController.stopLiveLocationSharing() doesn't create retain cycles or unexpected behavior when called after the view controller is deallocated.


🏁 Script executed:

#!/bin/bash
# Description: Check if stopLiveLocationSharing method properly handles controller lifecycle
# Expected: Find the method implementation and verify it handles weak references

# Search for stopLiveLocationSharing implementation
ast-grep --pattern 'func stopLiveLocationSharing() {
  $$$
}'

# Search for potential retain cycle patterns
rg -A 5 'stopLiveLocationSharing.*{' 

Length of output: 218


🏁 Script executed:

#!/bin/bash
set -e
# Find all occurrences of stopLiveLocationSharing (definition and calls) in the Swift codebase
rg -n "stopLiveLocationSharing" -C 3 --glob "*.swift"

Length of output: 13545


No retain cycle detected in onStopSharingTapped closure
The banner’s closure correctly captures self weakly, and calling messageController.stopLiveLocationSharing() doesn’t introduce any strong reference back to the view controller. No further changes are needed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (1)

899-969: Solid implementation with room for optimization.

The startLiveLocationSharing method is well-implemented with proper validation and conflict prevention. The async check for existing active live location messages correctly prevents duplicate live sharing.

However, consider addressing the TODO comment on line 927 - using ChatChannel.activeLiveLocations might be a more efficient approach than querying the message repository.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c07f7ad and 96e5fc6.

📒 Files selected for processing (3)
  • Sources/StreamChat/APIClient/Endpoints/Payloads/LocationPayloads.swift (1 hunks)
  • Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (6 hunks)
  • Sources/StreamChat/Workers/MessageUpdater.swift (28 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • Sources/StreamChat/APIClient/Endpoints/Payloads/LocationPayloads.swift
  • Sources/StreamChat/Workers/MessageUpdater.swift
🔇 Additional comments (6)
Sources/StreamChat/Controllers/ChannelController/ChannelController.swift (6)

71-73: LGTM! Clean property addition following existing patterns.

The channelMemberUpdater property is properly documented and follows the same pattern as other updater components in the class.


233-233: Consistent initialization pattern.

The channelMemberUpdater initialization follows the established dependency injection pattern used by other updaters in the controller.


844-898: Well-implemented static location method following established patterns.

The sendStaticLocation method correctly:

  • Validates channel state before proceeding
  • Uses comprehensive parameter documentation
  • Follows consistent error handling and callback patterns
  • Sets endAt: nil for static locations, which is appropriate

The implementation integrates well with the existing message creation infrastructure.


2043-2043: Good visibility improvement addressing previous feedback.

Making the ClientError extension public is appropriate as it allows customers to handle specific error types in their applications, which aligns with the previous review feedback.


2068-2077: Well-designed error class with meaningful context.

The ActiveLiveLocationAlreadyExists error class is properly implemented:

  • Includes the messageId of the conflicting message for better debugging and error handling
  • Provides a descriptive error message
  • Follows the established pattern of other ClientError subclasses

This gives developers clear information about what caused the conflict.


999-999: Minor formatting improvement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
✅ Feature An issue or PR related to a feature 🟢 QAed A PR that was QAed 🌐 SDK: StreamChat (LLC) Tasks related to the StreamChat LLC SDK
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants